Java多線程併發05——那麼多的鎖你都瞭解了嗎

在多線程或高併發情境中,經常會爲了保證數據一致性,而引入鎖機制,本文將爲各位帶來有關鎖的基本概念講解。關注我的公衆號「Java面典」瞭解更多 Java 相關知識點。

根據鎖的各種特性,可將鎖分爲以下幾類:

  • 樂觀鎖/悲觀鎖
  • 獨享鎖(互斥鎖)/共享鎖(讀寫鎖)
  • 可重入鎖
  • 公平鎖/非公平鎖
  • 分段鎖
  • 偏向鎖/輕量級鎖/重量級鎖
  • 自旋鎖

樂觀鎖/悲觀鎖

樂觀鎖與悲觀鎖並不是特指某兩種類型的鎖,是人們定義出來的概念或思想,主要是指看待併發同步的角度。

樂觀鎖

前提:認爲讀多寫少,遇到併發寫的可能性低,每次去拿數據的時候都認爲別人不會修改,所以不會上鎖;

實現:在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,採取在寫時先讀出當前版本號,然後加鎖操作(比較跟上一次的版本號,如果一樣則更新),如果失敗則要重複讀-比較-寫的操作。

應用:在 Java 中 java.util.concurrent.atomic 包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS(Compare and Swap 【比較並交換】)實現的。CAS 是一種更新的原子操作,比較當前值跟傳入值是否一樣,一樣則更新,否則失敗。

悲觀鎖

前提:認爲寫多,遇到併發寫的可能性高,每次去拿數據的時候都認爲別人會修改;

實現: 總是假設最壞的情況,以每次在讀寫數據的時候都會上鎖,這樣別人想讀寫這個數據就會阻塞直到拿到鎖;

應用:Java中的 Synchronized 就是悲觀鎖,AQS 框架下的鎖則是先嚐試 CAS 樂觀鎖去獲取鎖,獲取不到,纔會轉換爲悲觀鎖,如 RetreenLock。

小結

  • 悲觀鎖適合寫操作非常多的場景,樂觀鎖適合讀操作非常多的場景,不加鎖會帶來大量的性能提升;

  • 悲觀鎖在 Java 中的使用,就是利用各種鎖;

  • 樂觀鎖在 Java 中的使用,是無鎖編程,常常採用的是 CAS 算法,典型的例子就是原子類,通過 CAS 自旋實現原子操作的更新。

獨享鎖(互斥鎖)/共享鎖(讀寫鎖)

獨享鎖(互斥鎖)

定義: 獨享鎖是指該鎖一次只能被一個線程所持有;

特點:獨佔鎖是一種悲觀保守的加鎖策略,它避免了讀/讀衝突,如果某個只讀線程獲取鎖,則其他讀線程都只能等待,這種情況下就限制了不必要的併發性,因爲讀操作並不會影響數據的一致性。

應用:ReentrantLock 就是以獨佔方式實現的互斥鎖。

共享鎖(讀寫鎖)

定義:共享鎖是指該鎖可同時被多個線程所持有,併發訪問、共享資源;

特點:共享鎖則是一種樂觀鎖,它放寬了加鎖策略,允許多個執行讀操作的線程同時訪問共享資源;

應用

  1. AQS 的內部類 Node 定義了兩個常量 SHARED 和 EXCLUSIVE,他們分別標識 AQS 隊列中等待線程的鎖獲取模式。
  2. java 的併發包中提供了 ReadWriteLock,讀-寫鎖。它允許一個資源可以被多個讀操作訪問,或者被一個 寫操作訪問,但兩者不能同時進行。

可重入鎖(遞歸鎖)

定義:可重入鎖,也叫做遞歸鎖,指的是同一線程外層函數獲得鎖之後 ,內層遞歸函數仍然有獲取該鎖的代碼,但不受影響。

應用:在 JAVA 環境下 ReentrantLock 和 synchronized 都是可重入鎖。

公平鎖/非公平鎖

公平鎖

加鎖前檢查是否有排隊等待的線程,優先排隊等待的線程,先來先得。

非公平鎖

加鎖時不考慮排隊等待問題,直接嘗試獲取鎖,獲取不到自動到隊尾等待。

  1. 非公平鎖性能比公平鎖高 5~10 倍,因爲公平鎖需要在多核的情況下維護一個隊列;
  2. Java 中的 synchronized 是非公平鎖,ReentrantLock 默認的 lock()方法採用的是非公平鎖。

分段鎖

分段鎖也並非一種實際的鎖,而是一種思想 ConcurrentHashMap 是學習分段鎖的最好實踐。

偏向鎖/輕量級鎖/重量級鎖

這三種鎖是指鎖的狀態,並且是針對Synchronized。在Java 5通過引入鎖升級的機制來實現高效Synchronized。這三種鎖的狀態是通過對象監視器在對象頭中的字段來表明的。

偏向鎖

指一段同步代碼一直被一個線程所訪問,那麼該線程會自動獲取鎖。降低獲取鎖的代價。

輕量級鎖

指當鎖是偏向鎖的時候,被另一個線程所訪問,偏向鎖就會升級爲輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,提高性能。

重量級鎖

指當鎖爲輕量級鎖的時候,另一個線程雖然是自旋,但自旋不會一直持續下去,當自旋一定次數的時候,還沒有獲取到鎖,就會進入阻塞,該鎖膨脹爲重量級鎖。重量級鎖會讓他申請的線程進入阻塞,性能降低。

自旋鎖

在Java中,自旋鎖是指嘗試獲取鎖的線程不會立即阻塞,而是採用循環的方式去嘗試獲取鎖,這樣的好處是減少線程上下文切換的消耗,缺點是循環會消耗CPU。

特點

  1. 自旋鎖儘可能的減少線程的阻塞;
  2. 減少線程上下文切換的消耗,這對於鎖的競爭不激烈,且佔用鎖時間非常短的代碼塊來說性能能大幅度的提升;
  3. 如果鎖的競爭激烈,或者佔用鎖時間長短的代碼塊,不適合使用自旋鎖。**同時有大量線程在競爭一個鎖,會導致獲取鎖的時間很長,線程自旋的消耗大於線程阻塞掛起操作的消耗,其它需要 CPU 的線程又不能獲取到 CPU,造成 CPU 的浪費。所以這種情況下我們要關閉自旋鎖。

適應性自旋鎖

在 JDK1.5 及之前自旋時間是固定的,從 JDK1.6 開始,引入了適應性自旋鎖。

  • 特點
  1. 自旋時間由前一次在同一個鎖上的自旋時間以及鎖的擁有者的狀態來決定;
  2. 基本認爲一個線程上下文切換的時間是最佳的一個時間。
  • 優化
    JVM 還針對當前 CPU 的負荷情況做了較多的優化:
  1. 如果平均負載小於 CPUs 則一直自旋,如果有超過(CPUs/2)個線程正在自旋,則後來線程直接阻塞;
  2. 如果正在自旋的線程發現 Owner 發生了變化則延遲自旋時間(自旋計數)或進入阻塞;
  3. 如果 CPU 處於節電模式則停止自旋;
  4. 自旋時間的最壞情況是 CPU的存儲延遲(CPU A 存儲了一個數據,到 CPU B 得知這個數據直接的時間差),自旋時會適當放棄線程優先級之間的差異。

鎖的優化

在Java中,需要謹慎使用鎖。如無必要,不用最好;必須要用的話,也需要儘可能優化鎖的使用,以此來提高程序的吞吐量。關於鎖的優化,主要分爲應用方面的優化與 JVM 方面的優化,JVM方面的優化,一般不需要開發人員操心,開發人員更應該提升自身代碼素質,關注應用方面的優化。

應用優化

  • 減少鎖持有時間:只用在有線程安全要求的程序上加鎖;
  • 減小鎖粒度:將大對象(這個對象可能會被很多線程訪問),拆成小對象,大大增加並行度,降低鎖競爭。降低了鎖的競爭,偏向鎖,輕量級鎖成功率纔會提高。最最典型的減小鎖粒度的案例就是ConcurrentHashMap;
  • 鎖分離:最常見的鎖分離就是讀寫鎖 ReadWriteLock,根據功能進行分離成讀鎖和寫鎖,這樣讀讀不互斥,讀寫互斥,寫寫互斥,即保證了線程安全,又提高了性能。讀寫分離思想可以延伸,只要操作互不影響,鎖就可以分離。比如LinkedBlockingQueue 從頭部取出,從尾部放數據。

JVM優化

  • 鎖粗化:通常情況下,爲了保證多線程間的有效併發,會要求每個線程持有鎖的時間儘量短,即在使用完公共資源後,應該立即釋放鎖。但是,凡事都有一個度,如果對同一個鎖不停的進行請求、同步和釋放,其本身也會消耗系統寶貴的資源,反而不利於性能的優化 ;
  • 鎖消除:鎖消除是在編譯器級別的事情。在即時編譯器時,如果發現不可能被共享的對象,則可以消除這些對象的鎖操作,多數是因爲程序員編碼不規範引起。

多線程與併發系列推薦

Java多線程併發04——合理使用線程池

Java多線程併發03——什麼是線程上下文,線程是如何調度的

Java多線程併發02——線程的生命週期與常用方法,你都掌握了嗎

Java多線程併發01——線程的創建與終止,你會幾種方式

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