Java併發學習(二)

一、start和run的區別

在這裏插入圖片描述

  • start方法是創建一個新的子線程並啓動(調用run方法)
  • run方法只是Thread的一個普通方法的調用

二、線程的狀態

1. 新建(New): 創建後尚未啓動的線程的狀態

2. 運行(Runnable):包含Running和Ready

3. 無限期等待(Waiting): 不會被分配CPU執行時間,需要被喚醒

  • 沒有設置Timeout參數的Object.wait()方法
  • 沒有設置Timeout參數的Thread.join()方法
  • LockSupport.part()

4. 限期等待(timed waiting):在一定時間後會由系統自動喚醒

  • Thread.sleep()
  • 沒有設置Timeout參數的Object.wait() 方法
  • 沒有設置Timeout參數的Thread.join() 方法
  • LockSupport.parkNanos() 方法
  • LockSupport.parkUntil() 方法

5. 阻塞(Blocked): 等待獲取排他鎖

  • 就是在競爭鎖的時候,被阻塞了(例如因Lock 或者synchronize 關鍵字產生的狀態)

6. 終止(terminate):終止狀態

在這裏插入圖片描述

三、wait/notify/notifyAll

這幾個方法光知道是個啥,但理解的還不夠深入,這裏做一下總結。

1. wait/notify/notifyAll

在聊上面這個常見的方法之前,有必要先知道什麼是管程。

當我們對臨界區進行實現的時候,往往都是通過PV操作來實現的,但讓程序員手動去做PV操作,很容易發生死鎖。 所以爲了方便編程,減少死鎖出現的可能,我們希望能有一種數據結構或是軟件模塊來專門爲我們提供對“臨界區”的實現,這就是管程了~(但單單就說管程就是對臨界區的實現是不準確的,繼續往下看)

但僅僅是實現臨界區還是不夠的,比如,當線程A獲取到鎖了之後,進入了臨界區,這個時候因爲一些外部條件X, 而導致無法進行下去,這個時候就需要等待這個外部條件X的發生… 而假設這個外部條件X的發生是需要另一個線程B進入到當前的這個“臨界區”中才能觸發,而因爲線程A已經處於臨界區中了,所以線程B需要等待線程A退出臨界區才能繼續執行。。 於是。。就變成了線程A在等線程B,線程B在等線程A,死鎖出現了。。

因此,解決臨界區中的線程同步問題,也是管程需要實現的。

一個解決方案就是,在臨界區中的線程A一旦發現自己想要的外部條件沒有發生,而不能夠繼續進行下去了的時候,就主動釋放掉當前獲取的這個臨界區的鎖,然後讓其他線程進入到這個臨界區來觸發這個“外部條件X”的發生。。 等到這個外部條件X發生了之後,再通知線程A(之前因這個條件而釋放掉鎖的那個線程)重新去競爭鎖,繼續執行臨界區…

這個方法流程是不是很熟悉? 沒錯,這不就是wait和notify嘛。。

因此,管程的實現主要就是:

  • 臨界區的實現
  • monitor 對象及鎖的實現
  • 條件變量以及定義在 monitor 對象上的 wait,signal 操作的實現

然後就可以瞭解下Java對管程的實現了。

a. 對臨界區的實現

Synchronized的同步塊, ReentrantLock在lock和unlock期間的那段代碼… 都是對臨界區的實現…

在Java中,每個對象都有兩個池,鎖(monitor)池和等待池

鎖池:假設線程A已經擁有了某個對象(注意:不是類)的鎖,而其它的線程想要調用這個對象的某個synchronized方法(或者synchronized塊),由於這些線程在進入對象的synchronized方法之前必須先獲得該對象的鎖的擁有權,但是該對象的鎖目前正被線程A擁有,所以這些線程就進入了該對象的鎖池中。

等待池:假設一個線程A調用了某個對象的wait()方法,線程A就會釋放該對象的鎖(因爲wait()方法必須出現在synchronized中,這樣自然在執行wait()方法之前線程A就已經擁有了該對象的鎖),同時線程A就進入到了該對象的等待池中。如果另外的一個線程調用了相同對象的notifyAll()方法,那麼處於該對象的等待池中的線程就會全部進入該對象的鎖池中,準備爭奪鎖的擁有權。如果另外的一個線程調用了相同對象的notify()方法,那麼僅僅有一個處於該對象的等待池中的線程(隨機)會進入該對象的鎖池.

b. 條件變量以及定義在 monitor 對象上的 wait,signal 操作的實現

對於Synchronized,只實現了wait和signal操作…
如果想使用更細粒度的條件變量,來控制臨界區內線程的同步,那麼可以使用ReentrantLock來做…

ReentrantLock提供了Condition變量,作爲條件變量,對應的方法是 condition.await() 和 condition.signal()

鎖池和等待池

在Java中,每個對象都有兩個池,鎖(monitor)池和等待池

鎖池: 假設線程A已經擁有了某個對象(注意:不是類)的鎖,而其它的線程想要調用這個對象的某個synchronized方法(或者synchronized塊),由於這些線程在進入對象的synchronized方法之前必須先獲得該對象的鎖的擁有權,但是該對象的鎖目前正被線程A擁有,所以這些線程就進入了該對象的鎖池中。

等待池: 假設一個線程A調用了某個對象的wait()方法,線程A就會釋放該對象的鎖(因爲wait()方法必須出現在synchronized中,這樣自然在執行wait()方法之前線程A就已經擁有了該對象的鎖),同時線程A就進入到了該對象的等待池中。如果另外的一個線程調用了相同對象的notifyAll()方法,那麼處於該對象的等待池中的線程就會全部進入該對象的鎖池中,準備爭奪鎖的擁有權。如果另外的一個線程調用了相同對象的notify()方法,那麼僅僅有一個處於該對象的等待池中的線程(隨機)會進入該對象的鎖池。

wait原理:

如果線程調用了對象的 wait()方法,那麼線程便會處於該對象的等待池中,等待池中的線程不會去競爭該對象的鎖。
當有線程調用了對象的 notifyAll()方法(喚醒所有 wait 線程)或 notify()方法(只隨機喚醒一個 wait 線程),被喚醒的的線程便會進入該對象的鎖池中,鎖池中的線程會去競爭該對象鎖。
優先級高的線程競爭到對象鎖的概率大,假若某線程沒有競爭到該對象鎖,它還會留在鎖池中,唯有線程再次調用 wait()方法,它纔會重新回到等待池中。而競爭到對象鎖的線程則繼續往下執行,直到執行完了 synchronized 代碼塊,它會釋放掉該對象鎖,這時鎖池中的線程會繼續競爭該對象鎖。

必讀:
sleep()和wait()方法與對象鎖、鎖池、等待池

Thread.yield()和Thread.sleep(0)

推薦閱讀

2. wait和park的區別

我們在編程的時候必須能保證wait方法比notify方法先執行。如果notify方法比wait方法晚執行的話,就會導致因wait方法進入休眠的線程接收不到喚醒通知的問題。

而park、unpark則不會有這個問題,我們可以先調用unpark方法釋放一個許可證,這樣後面線程調用park方法時,發現已經許可證了,就可以直接獲取許可證而不用進入休眠狀態了。

LockSupport.park() 的實現原理是通過二元信號量做的阻塞,要注意的是,這個信號量最多隻能加到1。我們也可以理解成獲取釋放許可證的場景。unpark()方法會釋放一個許可證,park()方法則是獲取許可證,如果當前沒有許可證,則進入休眠狀態,知道許可證被釋放了才被喚醒。無論執行多少次unpark()方法,也最多隻會有一個許可證。

另外,和wait方法不同,執行park進入休眠後並不會釋放持有的鎖。
並且,調用wait方法需要已經獲取到鎖,而park則不需要

四、wait的侷限,以及Condition的出場

使用wait的一個前提就是在sync的同步塊裏,而這又導致了在同步塊裏的條件變量只有一個,儘管可以通過共享變量的方式來實現“需要多個條件變量的場合”,但這樣不僅實現的複雜度高,而且也不是很高效。因此,爲了讓在同步塊中使用更多樣的條件變量(即對某一資源或者某一個事件的等待),ReentrantLock就提供了Condition這一個神器,一個Lock可以new出多個Condition,即多個等待隊列。

所以,await/signal, 可以看成強化版的 wait/notify

參考鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章