多線程併發 — 你應該瞭解的線程鎖

回顧操作系統的發展歷史:手工操作(串行執行程序) —> 批處理系統(自動地、成批地處理一個或多個用戶的作業) —> 多道程序設計技術(允許多個程序同時進入內存並交替在CPU中運行) —> 分時系統(採用時間片輪轉的方式同時爲幾個、幾十個甚至幾百個用戶服務) —> 實時系統 —> ...,從操作系統的發展可以看出來,從單任務到多任務,從多道處理到分時處理,計算機的資源利用率和併發性越來越高了。

爲了提高處理器資源的利用率提高系統的吞吐率,基本上都採用多線程和併發的運作方式。
併發(Concurrency):是指在在同一時間間隔(即某個時間段內),多任務交替處理的能力(理想情況下的同時)。CPU把可執行時間均勻地分成若干份,每個進程執行一段時間後,記錄當前的工作狀態,釋放相關的執行資源並進入等待狀態,讓其他線程搶佔CPU資源。
並行(Parallelism):指在同一時刻,有多條指令在多個處理器上同時執行,也就是真正意義上的同時。

1. 多線程開發爲什麼要用鎖?

鎖-是爲了解決多線程併發操作引起的髒讀、數據不一致的問題。

疑問:
那麼爲什麼多線程併發操作會引起髒讀、數據不一致的問題?

  1. Java 內存模型規定了所有的變量都存儲在主內存中,每條線程有自己的工作內存。
  2. 線程的工作內存中保存了該線程中用到的變量的主內存副本拷貝,線程對變量的所有操作都必須在工作內存中進行,而不是直接讀寫主內存。
  3. 線程訪問一個變量,首先將變量從主內存拷貝到工作內存,對變量的寫操作,不會馬上同步到主內存。
  4. 不同的線程之間也無法直接訪問對方工作內存中的變量,線程間變量的傳遞均需要自己的工作內存和主存之間進行數據同步進行。

2. 線程鎖的分類

  • 2.1、公平鎖與非公平鎖

    • 公平鎖:
      是指多個線程在等待同一個鎖時,必須按照申請鎖的先後順序來獲得鎖,不允許其他線程插隊獲得鎖。
    • 非公平鎖:
      是允許插隊獲得鎖。

    優缺點:
    非公平鎖性能高於公平鎖性能,非公平鎖能更充分的利用cpu的時間片,儘量的減少cpu空閒的狀態時間。但是使用非公平鎖有些線程可能會餓死或者說很早就在等待鎖,但要等很久纔會獲得鎖,而使用公平鎖等待鎖的線程不會餓死。
    具體表現形式:
    ReentrantLock可以指定是公平鎖還是非公平鎖。
    而synchronized只能是非公平鎖。

  • 2.2、可重入鎖與不可重入鎖

    • 可重入鎖:
      指的是可重複可遞歸調用的鎖,在外層函數獲得鎖之後,在內層遞歸函數仍然再次獲得之前已經獲得的鎖,並且不發生死鎖(前提得是同一個對象或者class),這樣的鎖就叫做可重入鎖。
      好處:
      一定程度上避免產生死鎖。
    • 不可重入鎖:
      和可重入鎖相反,指的是同一線程外層函數獲得鎖之後,那麼在內層遞歸函數不能再次獲取該鎖而被阻塞。

    概念區分:當一個線程獲得當前實例的鎖lock,並且進入了方法A,該線程在方法A沒有釋放該鎖的時候,是否可以再次進入使用該鎖的方法B?
    不可重入鎖:在方法A釋放鎖之前,不可以再次進入方法B
    可重入鎖:在方法A釋放該鎖之前可以再次進入方法B。
    具體表現形式:
    ReentrantLock 和synchronized 都是可重入鎖。

  • 2.3、自旋鎖與阻塞鎖

    • 自旋鎖:
      當一條線程需要請求一把已經被佔用的鎖時,並不會進入阻塞狀態,而是繼續持有CPU執行權等待一段時間,去執行一次空循環,循環結束後再重新去競爭鎖,如果在自旋完成後前面鎖同步資源的線程已經釋放了鎖,那麼當前線程就可以不必阻塞而是直接獲取同步資源,從而避免了切換線程的開銷,如果競爭不到則繼續循環。

      • 背景:
        互斥同步對性能最大的影響是阻塞,掛起和恢復線程都需要轉入內核態中完成,虛擬機爲了避免線程真實的在操作系統層面上被掛起,這個時候可以利用自旋鎖的優化手段。通常情況下,線程持有鎖的時間都不會太長也就是共享數據的鎖定狀態只持續很短的一段時間,如果直接掛起操作系統層面的線程可能會得不償失,畢竟操作系統實現線程之間轉換需要從用戶態轉化爲核心態,這個狀態之間的轉化相對比較耗時,因此自旋鎖會假設在不久的將來當前的線程就可以獲得鎖,因此虛擬機會讓當前想要獲取的線程做幾個空的循環,一般不會太久,在經過一段時間的循環之後再重新去競爭鎖,如果獲得了鎖,就可以進入到了臨界區。如果還不能獲取到鎖,那麼就會將線程在操作系統層面上掛起,這就是自旋鎖的優化方式,這種方式確實也是可以提升效率的,最後沒有辦法就只能升級爲重量級鎖。
      • 優點:
        由於自旋等待鎖的過程線程並不會引起上下文切換(用戶態轉向核心態),因此比較高效;
      • 缺點:
        自旋等待過程線程一直佔用CPU執行權但不處理任何任務,因此若該過程過長,那就會造成CPU資源的浪費。
      • 自適應自旋:
        自適應自旋可以根據以往自旋等待時間的經驗,計算出一個較爲合理的本次自旋等待時間。
    • 阻塞鎖:
      和自旋鎖相對,指當線程獲取鎖失敗時,線程進入阻塞(blocking)狀態,當獲取相應的信號時(喚醒,時間),進入線程的準備就緒狀態,準備就緒狀態的所有線程,通過競爭,進入運行狀態。

    適用情況:
    自旋等待不能代替阻塞。自旋等待雖然不處理任何任務,但它是要佔用處理器時間的,因此,如果鎖被佔用的時間很短,自旋效果就會非常好,線程不會進行上下文切換(用戶態轉向核心態),反之,如果鎖被佔用的時間很長,那麼自旋的線程只會造成CPU資源的浪費。

  • 2.4、樂觀鎖與悲觀鎖

    • 樂觀鎖:
      樂觀鎖認爲自己在使用數據時不會有別的線程修改數據,所以不會添加鎖,只是在更新數據的時候去判斷之前有沒有別的線程更新了這個數據。如果這個數據沒有被更新,當前線程將自己修改的數據成功寫入。如果數據已經被其他線程更新發生衝突,那麼就應該有相應的重試邏輯。
      具體表現形式:java的原子類的遞增操作
      原理:採用CAS算法

    • 悲觀鎖:
      悲觀鎖認爲自己在使用數據的時候一定有別的線程來修改數據,因此在獲取數據的時候會先加鎖,確保數據不會被別的線程修改,是“先取鎖再訪問”的保守策略,爲數據處理的安全提供了保證。
      具體表現形式:synchronized關鍵字和lock實現類
      在效率方面,處理加鎖的機制會產生額外的開銷,還有增加產生死鎖的機會,同時也會降低併發性能。

    使用場景:
    悲觀鎖適合寫操作多的場景,先加鎖可以保證寫操作時數據正確。
    樂觀鎖適合讀操作多的場景,不加鎖的特點能夠使其讀操作的性能大幅提升。

  • 2.5、互斥鎖與共享鎖

    • 互斥鎖:
      同時只能有一個線程獲得鎖。
      具體表現形式:
      ReentrantLock 是互斥鎖,ReadWriteLock 中的寫鎖是互斥鎖。

    • 共享鎖:
      可以有多個線程同時獲得鎖。
      具體表現形式:
      Semaphore、CountDownLatch 是共享鎖,ReadWriteLock 中的讀鎖是共享鎖。

  • 2.6、偏向鎖、輕量級鎖及重量級鎖

    • 偏向鎖
      偏向鎖是爲了消除無競爭情況下的同步原語,進一步提升程序性能。
      與輕量級鎖的區別:
      輕量級鎖是在無競爭的情況下使用CAS操作來代替互斥量的使用,從而實現同步;而偏向鎖是在無競爭的情況下完全取消同步。
      與輕量級鎖的相同點:
      它們都是樂觀鎖,都認爲同步期間不會有其他線程競爭鎖。
      原理:
      當線程請求到鎖對象後,將鎖對象的狀態標誌位改爲01,即偏向模式。然後使用CAS操作將線程的ID記錄在鎖對象的Mark Word中。以後該線程可以直接進入同步塊,連CAS操作都不需要。但是,一旦有第二條線程需要競爭鎖,那麼偏向模式立即結束,進入輕量級鎖的狀態。
      優點:
      偏向鎖可以提高有同步但沒有競爭的程序性能。但是如果鎖對象時常被多條線程競爭,那偏向鎖就是多餘的。
      偏向鎖可以通過虛擬機的參數來控制它是否開啓。

    • 輕量級鎖
      本質:
      使用CAS取代互斥同步。
      輕量級鎖與重量級鎖的比較:
      重量級鎖是一種悲觀鎖,它認爲總是有多條線程要競爭鎖,所以它每次處理共享數據時,不管當前系統中是否真的有線程在競爭鎖,它都會使用互斥同步來保證線程的安全;
      而輕量級鎖是一種樂觀鎖,它認爲鎖存在競爭的概率比較小,所以它不使用互斥同步,而是使用CAS操作來獲得鎖,這樣能減少互斥同步所使用的互斥量帶來的性能開銷,但是如果始終得不到鎖競爭的線程使用自旋會消耗CPU。
      實現原理:

      1. 對象頭稱爲Mark Word,虛擬機爲了節約對象的存儲空間,對象處於不同的狀態下,Mark Word中存儲的信息也所有不同。
      2. Mark Word中有個標誌位用來表示當前對象所處的狀態。
      3. 當線程請求鎖時,若該鎖對象的Mark Word中標誌位爲01(未鎖定狀態),則在該線程的棧幀中創建一塊名爲鎖記錄的空間,然後將鎖對象的Mark Word拷貝至該空間;最後通過CAS操作將鎖對象的Mark Word指向該鎖記錄;
      4. 若CAS操作成功,則輕量級鎖的上鎖過程成功;
      5. 若CAS操作失敗,再判斷當前線程是否已經持有了該輕量級鎖;若已經持有,則直接進入同步塊;若尚未持有,則表示該鎖已經被其他線程佔用,此時輕量級鎖就要膨脹成重量級鎖。

      注意:輕量級鎖比重量級鎖性能更高的前提是,在輕量級鎖被佔用的整個同步週期內,不存在其他線程的競爭。若在該過程中一旦有其他線程競爭,那麼就會膨脹成重量級鎖,從而除了使用互斥量以外,還額外發生了CAS操作,因此更慢!

    • 重量級鎖
      在JVM中又叫對象監視器(Monitor),它還負責實現了Semaphore(信號量)的功能,也就是說它至少包含一個競爭鎖的隊列,和一個信號阻塞隊列(wait隊列),前者負責做互斥,後者用於做線程同步。
      整個synchronized鎖流程如下:

      1. 檢測Mark Word裏面是不是當前線程的ID,如果是,表示當前線程處於偏向鎖
      2. 如果不是,則使用CAS將當前線程的ID替換Mard Word,如果成功則表示當前線程獲得偏向鎖,置偏向標誌位1
      3. 如果失敗,則說明發生競爭,撤銷偏向鎖,進而升級爲輕量級鎖。
      4. 當前線程使用CAS將對象頭的Mark Word替換爲鎖記錄指針,如果成功,當前線程獲得鎖
      5. 如果失敗,表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。
      6. 如果自旋成功則依然處於輕量級狀態。
      7. 如果自旋失敗,則升級爲重量級鎖。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章