Java並發編程:如何防止在線程阻塞與喚醒時死鎖

2020-12-17 騰訊網

Java並發編程:多線程如何實現阻塞與喚醒說到suspend與resume組合有死鎖傾向,一不小心將導致很多問題,甚至導致整個系統崩潰。接著看另外一種解決方案,我們可以使用以對象為目標的阻塞,即利用Object類的wait()和notify()方法實現線程阻塞。當線程到達監控對象時,通過wait方法會使線程進入到等待隊列中。而當其它線程調用notify時則可以使線程重新回到執行隊列中,得以繼續執行

01

思維不同

針對對象的阻塞編程思維需要我們稍微轉變下思維,它與面向線程阻塞思維有較大差異。如前面的suspend與resume只需在線程內直接調用就能完成掛起恢復操作,這個很好理解。而如果改用wait與notify形式則是通過一個object作為信號,可以將其看成是一堵門。object的wait()方法是鎖門的動作,notify()是開門的動作。某一線程一旦關上門後其他線程都將阻塞,直到別的線程打開門。

如圖所示,一個對象object調用wait()方法則像是堵了一扇門。線程一、線程二都將阻塞,然後線程三調用object的notify()方法打開門,準確地說是調用了notifyAll()方法,notify()僅僅能讓線程一或線程二其中一條線程通過)。最終線程一、線程二得以通過。

02

死鎖問題解決了嗎?

使用wait與notify能在一定程度上避免死鎖問題,但並不能完全避免,它要求我們必須在編程過程中避免死鎖。在使用過程中需要注意的幾點是:

首先,wait與notify方法是針對對象的,調用任意對象的wait()方法都將導致線程阻塞,阻塞的同時也將釋放該對象的鎖。相應地,調用任意對象的notify()方法則將隨機解除該對象阻塞的線程,但它需要重新獲取改對象的鎖,直到獲取成功才能往下執行。

其次,wait與notify方法必須在synchronized塊或方法中被調用,並且要保證同步塊或方法的鎖對象與調用wait與notify方法的對象是同一個。如此一來在調用wait之前當前線程就已經成功獲取某對象的鎖,執行wait阻塞後當前線程就將之前獲取的對象鎖釋放。當然假如你不按照上面規定約束編寫,程序一樣能通過編譯,但運行時將拋出IllegalMonitorStateException異常,必須在編寫時保證用法正確。

最後,notify是隨機喚醒一條阻塞中的線程並讓之獲取對象鎖,進而往下執行,而notifyAll則是喚醒阻塞中的所有線程,讓他們去競爭該對象鎖,獲取到鎖的那條線程才能往下執行。

03

改進例子

04

Park與UnPark

wait與notify組合的方式看起來是個不錯的解決方式,但其面向的主體是對象object,阻塞的是當前線程,而喚醒的是隨機的某個線程或所有線程,偏重於線程之間的通信交互。假如換個角度,面向的主體是線程的話,我就能輕而易舉地對指定的線程進行阻塞喚醒,這個時候就需要LockSupport,它提供的park與unpark方法分別用於阻塞和喚醒.而且它提供避免死鎖和競態條件,很好地代替suspend和resume組合。

05

LockSupport優勢

LockSupport類為線程阻塞喚醒提供了基礎,同時,在競爭條件問題上具有wait和notify無可比擬的優勢。使用wait和notify組合時,某一線程在被另一線程notify之前必須要保證此線程已經執行到wait等待點,錯過notify則可能永遠都在等待,另外notify也不能保證喚醒指定的某線程。反觀LockSupport,由於park與unpark引入了許可機制,許可邏輯為:

park將許可在等於0的時候阻塞,等於1的時候返回並將許可減為0。

unpark嘗試喚醒線程,許可加1。

根據這兩個邏輯,對於同一條線程,park與unpark先後操作的順序似乎並不影響程序正確地執行。假如先執行unpark操作,許可則為1,之後再執行park操作,此時因為許可等於1直接返回往下執行,並不執行阻塞操作。 最後,LockSupport的park與unpark組合真正解耦了線程之間的同步,不再需要另外的對象變量存儲狀態,並且也不需要考慮同步鎖,wait與notify要保證必須有鎖才能執行,而且執行notify操作釋放鎖後還要將當前線程扔進該對象鎖的等待隊列,LockSupport則完全不用考慮對象、鎖、等待隊列等問題。

- END -

相關焦點

  • Java並發編程:多線程如何實現阻塞與喚醒
    線程的阻塞和喚醒在多線程並發過程中是一個關鍵點,當線程數量達到很大的數量級時,並發可能帶來很多隱蔽的問題。如何正確暫停一個線程,暫停後又如何在一個要求的時間點恢復,這些都需要仔細考慮的細節。
  • 「原創」Java並發編程系列09|基礎乾貨
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫本文為何適原創並發編程系列第9篇。現在,我們進入正題:介紹並發編程的基礎性概念。
  • 原創】Java並發編程系列01|開篇獲獎感言
    ,他剛工作時的並發編程第一原則就是不要寫並發程序。所以,並發編程已經成為一項必備技能。並發編程是Java語言的重要特性之一,它能使複雜的代碼變得更簡單,從而極大地簡化複雜系統的開發。並發編程可以充分發揮多處理器系統的強大計算能力,隨著處理器數量的持續增長,如何高效的並發變得越來越重要。
  • 「原創」Java並發編程系列07|synchronized原理
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫並發編程中用到最多的關鍵字毫無疑問是synchronized。這篇文章就來探究下synchronized:synchronized如何使用?
  • JAVA高並發網絡編程之BIO堵塞網絡編程
    上次說了網絡編程都是有作業系統統一的API的,每個語言有對它的實現,這次來一起說說通過java原生的socket編程完成BIO的網絡編程。
  • Java並發編程系列20|StampedLock源碼解析
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫本文為何適原創並發編程系列第 20 篇,文末有本系列文章匯總。也就是 ABA 問題,在【原創】Java 並發編程系列 12 | 揭秘 CAS文章中介紹過 ABA 問題的解決辦法就是加版本號,將原來的 A->B->A 就變成了 1A->2B->3A。
  • JAVA並發編程:並發問題的根源及主要解決方法
    計算機的發展有一部分就是如何重複利用資源,解決硬體資源之間效率的不平衡,而後就有了多進程,多線程的發展。線程切換帶來的原子性為了更充分得利用CPU,引入了CPU時間片時間片的概念。進程或線程通過爭用CPU時間片,讓CPU可以更加充分地利用。比如在進行讀寫磁碟等耗時高的任務時,就可以將寶貴的CPU資源讓出來讓其他線程去獲取CPU並執行任務。
  • Java並發編程系列23|循環屏障CyclicBarrier
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫本篇介紹第二個並發工具類CyclicBarrier,CyclicBarrier的字面意思是可循環使用(Cyclic)的屏障(Barrier),分以下部分介紹:CyclicBarrier的使用CyclicBarrier與CountDownLatch
  • 「原創」Java並發編程系列14|AQS源碼分析
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫本文為何適原創並發編程系列第 14 篇,文末有本系列文章匯總。AbstractQueuedSynchronizer是Java並發包java.util.concurrent的核心基礎組件,是實現Lock的基礎。
  • 「原創」Java並發編程系列18|讀寫鎖(下)
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫本文為何適原創並發編程系列第 18 篇,文末有本系列文章匯總。上篇為【原創】Java並發編程系列17 | 讀寫鎖八講(上),沒看過的可以先看看。本文是下篇,從「源碼分析寫鎖的獲取與釋放」開始。7.
  • 源碼解析:阻塞隊列 LinkedBlockingQueue
    takeLock用於控制出隊的並發,putLock用於入隊的並發。這也就意味著,同一時刻,只能只有一個線程能執行入隊/出隊操作,其餘入隊/出隊線程會被阻塞;但是,入隊和出隊之間可以並發執行,即同一時刻,可以同時有一個線程進行入隊,另一個線程進行出隊,這樣就可以提升吞吐量。
  • 「原創」Java並發編程系列02|並發編程三大核心問題
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫寫在前面編寫並發程序是比較困難的,因為並發程序極易出現Bug,這些Bug有都是比較詭異的,很多都是沒辦法追蹤,而且難以復現。
  • Java並發編程系列21|Condition-Lock的等待通知
    ,線程進入AQS同步隊列後被喚醒,繼續從這裡阻塞的地方開始執行 * 4.注意這裡while循環的自旋,線程被喚醒以後還要再檢查一下node是否在AQS同步隊列中 */ while (!
  • 「原創」Java並發編程系列33|深入理解線程池(上)
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫並發編程必不可少的線程池,接下來分兩篇文章介紹線程池,本文是第一篇。介紹1.1 使用場景並發編程可以高效利用CPU資源,提升任務執行效率,但是多線程及線程間的切換也伴隨著資源的消耗。當遇到單個任務處理時間比較短,但需要處理的任務數量很大時,線程會頻繁的創建銷毀,大量的時間和資源都會浪費在線程的創建和銷毀上,效率很低。
  • Java從零開始學 - 第25天:掌握JUC中的阻塞隊列
    BlockingQueue接口BlockingQueue位於juc中,熟稱阻塞隊列, 阻塞隊列首先它是一個隊列,繼承Queue接口,是隊列就會遵循先進先出(FIFO)的原則,又因為它是阻塞的,故與普通的隊列有兩點區別:當一個線程向隊列裡面添加數據時,如果隊列是滿的,那麼將阻塞該線程,暫停添加數據當一個線程從隊列裡面取出數據時,如果隊列是空的,那麼將阻塞該線程
  • Java是如何實現Future模式的?萬字詳解!
    我們現在著重來分析下run方法:// FutureTask.javapublicvoidrun(){// 【1】,為了防止多線程並發執行異步任務,這裡需要判斷線程滿不滿足執行異步任務的條件,有以下三種情況:// 1)若任務狀態state
  • Java 內存模型與線程
    在命令式編程中,線程之間的通信機制有兩種:共享內存和消息傳遞。線程同步是指程序用於控制不同線程之間操作發生相對順序的機制。Java 內存模型規定了如何和何時可以看到由其他線程修改過後的共享變量的值,以及在必須時如何同步的訪問共享變量。
  • 淺談Java線程狀態轉換及控制
    補充一下sleep、yield、join和wait的差異:  ① sleep、join、yield時並不釋放對象鎖資源,在wait操作時會釋放對象資源,wait在被notify/notifyAll喚醒時,重新去搶奪獲取對象鎖資源。
  • Java並發編程:CountDownLatch、CyclicBarrier和Semaphore
    在java 1.5中,提供了一些非常有用的輔助類來幫助我們進行並發編程,比如CountDownLatch,CyclicBarrier和Semaphore,今天我們就來學習一下這三個輔助類的用法。barrier狀態;參數barrierAction為當這些線程都達到barrier狀態時會執行的內容。
  • Python並發編程很簡單,一文幫你搞清如何創建線程類
    對於Python的並發編程相關的東東,相信通過上次咱們的探討,大家已經比較清楚了,對於Python創建線程的方式主要有兩種,這個上次咱們也已經說過了哦,第一種是使用threading模塊的Thread類的構造器來創建線程,這種方式上次咱們已經詳細討論過了哦,這次呢,咱們就重點和大家來聊聊第二種方式吧