JAVA多執行緒與鎖機制

iterationjia發表於2021-03-06

JAVA多執行緒與鎖機制

1 關於Synchronized和lock

  • synchronized是Java的關鍵字,當它用來修飾一個方法或者一個程式碼塊的時候,能夠保證在同一時刻最多隻有一個執行緒執行該段程式碼

    • JDK1.5以後引入了自旋鎖、鎖粗化、輕量級鎖,偏向鎖來有優化關鍵字的效能。
    • 一個執行緒訪問一個被synchronized修飾的程式碼塊,會自動獲取對應的一個鎖,並在執行該程式碼塊時,其他執行緒想訪問這個程式碼塊,會一直處於等待狀態,自有等該執行緒釋放鎖後,其他執行緒進行資源競爭,競爭獲取到鎖的執行緒才能訪問該程式碼塊。
  • synchronized在發生異常時,會自動釋放執行緒佔有的鎖,因此不會導致死鎖現象發生;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,因此使用Lock時需要在finally塊中釋放鎖;

  • Lock可以讓等待鎖的執行緒響應中斷,而synchronized卻不行,使用synchronized時,等待的執行緒會一直等待下去,不能夠響應中斷;

  • 通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到。

  • Lock鎖適合大量同步程式碼的同步問題,synchronized鎖適合程式碼少量的同步問題。總體上來說,在資源競爭不激烈的情形下,效能稍微比synchronized差點。但是資源競爭非常激烈的時候,synchronized的效能會下降很多,而ReentrantLock的效能表現仍然比較穩定。

2 Java 實現執行緒安全的三種方式

  • 同步程式碼塊
synchronized(obj)
{
    //需要被同步的程式碼塊
}

obj 稱為同步監視器,也就是鎖,原理是:當執行緒開始執行同步程式碼塊前,必須先獲得對同步程式碼塊的鎖定。並且任何時刻只能有一個執行緒可以獲得對同步監視器的鎖定,當同步程式碼塊執行完成後,該執行緒會釋放對該同步監視器的鎖定。

  • 同步方法
public synchronized void testThread()
{
    //需要被同步的程式碼塊
}

不需要再指定同步監視器,這個同步方法(非static方法)無需顯式地指定同步監視器,同步方法的同步監視器就是this,也就是呼叫該方法的物件。

  • 同步鎖,Lock鎖機制, 通過建立Lock物件,採用lock()加鎖,unlock()解鎖,來保護指定的程式碼塊。其中,為了確保能夠在必要的時候釋放鎖,程式碼中使用finally來確保鎖的釋放,來防止死鎖!

3 Synchronized

  • static synchronized則是該類的所有例項公用一個監視塊,靜態同步方法和非靜態同步方法持有的是不同的鎖,前者是類鎖,後者是物件鎖

  • 1.方法宣告時使用:執行緒獲得的是成員鎖

    2.對某一程式碼塊使用:執行緒獲得的是成員鎖

    3.synchronized後面括號裡是一物件,此時,執行緒獲得的是物件鎖

    4.synchronized後面括號裡是類,此時,執行緒獲得的是物件鎖

4 樂觀鎖、悲觀鎖

  • 樂觀鎖:總是假設最好的情況,每次去拿資料的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個資料。

    • 可以使用版本號機制(+遞迴)和CAS演算法實現。如果發現資料已經被更改(通過版本號控制),則不更新資料,再次去重複 所需操作直到道沒有衝突(使用遞迴演算法)。
  • 悲觀鎖:總是假設最壞的情況,每次去拿資料的時候都認為別人會修改,所以每次在拿資料的時候都會上鎖,這樣別人想拿這個資料就會阻塞直到它拿到鎖。

4 公平鎖、非公平鎖

公平鎖:多個執行緒按照申請鎖的順序去獲得鎖,執行緒會直接進入佇列去排隊,永遠都是佇列的第一位才能得到鎖。

  • 優點:所有的執行緒都能得到資源,不會餓死在佇列中。
  • 缺點:吞吐量會下降很多,佇列裡面除了第一個執行緒,其他的執行緒都會阻塞,cpu喚醒阻塞執行緒的開銷會很大

非公平鎖:多個執行緒去獲取鎖的時候,會直接去嘗試獲取,獲取不到,再去進入等待佇列,如果能獲取到,就直接獲取到鎖。

  • 優點:可以減少CPU喚醒執行緒的開銷,整體的吞吐效率會高點,CPU也不必取喚醒所有執行緒,會減少喚起執行緒的數量。
  • 缺點:你們可能也發現了,這樣可能導致佇列中間的執行緒一直獲取不到鎖或者長時間獲取不到鎖,導致餓死

5 JAVA執行緒池

  • 如果併發的執行緒數量很多,並且每個執行緒都是執行一個時間很短的任務就結束了,這樣頻繁建立執行緒就會大大降低系統的效率,因為頻繁建立執行緒和銷燬執行緒需要時間。執行緒池使得執行緒可以複用,就是執行完一個任務,並不被銷燬,而是可以繼續執行其他的任務。

  • 提高系統響應速度,降低系統資源消耗

  • 引數:

    • 一個任務被提交到執行緒池以後,首先會找有沒有空閒存活執行緒(>=corePoolSize的情況)

      • 如果有則直接將任務交給這個空閒執行緒來執行,
      • 如果沒有則會快取到工作佇列中,如果工作佇列滿了,才會建立一個新執行緒,然後從工作佇列的頭部取出一個任務交由新執行緒來處理,而將剛提交的任務放入工作佇列尾部。執行緒池不會無限制的去建立新執行緒,它會有一個最大執行緒數量的限制,這個數量即由maximunPoolSize指定。
    • corePoolSize(核心執行緒數):

      • 核心執行緒會一直存活,即使沒有任務需要執行
      • 當執行緒數小於核心執行緒數時,即使有執行緒空閒,執行緒池也會優先建立新執行緒處理
    • maxPoolSize(最大執行緒數):

      • 當執行緒數=maxPoolSize,且任務佇列已滿時,執行緒池會拒絕處理任務而丟擲異常
    • keepAliveTime(執行緒存活保持時間):

      • 當執行緒池中執行緒數大於核心執行緒數時,執行緒的空閒時間如果超過執行緒存活時間,那麼這個執行緒就會被銷燬,直到執行緒池中的執行緒數小於等於核心執行緒數。
    • workQueue (工作佇列):

      新任務被提交後,會先進入到此工作佇列中,任務排程時再從佇列中取出任務。jdk中提供了四種工作佇列:

    • handler(執行緒飽和策略):

      • 當執行緒池和佇列都滿了,再加入執行緒會執行此策略。

6 多執行緒中的i++執行緒安全嗎

不安全。i++不是原子性操作。i++分為讀取i值,對i值加一,再賦值給i++,執行期中任何一步都是有可能被其他執行緒搶佔的。

相關文章