可愛的程序員喲,你忘掉的是這堆Java鎖呢?還是這把死鎖呢?

經過了幾天的面試系列講解,前面已經講完了Java基礎、Java集合和JVM,上一篇又講到了Java多線程併發系列中的線程池、線程生命週期等內容:

推薦閱讀:

《面試官,Java多線程併發我能講3個小時,你確定要聽?》

文末還有福利!!!記得去看看哦!!!

可愛的程序員喲,你忘掉的是這堆Java鎖呢?還是這把死鎖呢?

 

​面試能講3個小時的內容,當然不是一兩篇文章就能寫完的,這一篇咱們再來講一講...

小故事:

很久很久以前,有一個夥計,在他掌櫃那裏算完賬,已經到了三更半夜纔回家,手上拿着一把用了十年的算盤。

 

走着走着,就來到了他每天上班途中都要經過的河邊,一不小心被石頭絆了一下,算盤被扔進了漆黑的河裏。這個夥計非常着急,拿個算盤可是他賴以生存的“寶貝”。

 

這時,河神從河裏冒了出來,開口問這個夥計:“可愛的夥計喲,你掉在河裏的是這個金算盤呢?還是這個銀算盤呢?”

 

夥計十分誠實,搖了搖頭,我掉的是一把破木算盤。河神被這個夥計的誠實打動了,把金、銀算盤都送給了他。

後來,過了很久很久,有一個程序員到了凌晨2點才下班,拿着筆記本電腦,走在每天上下班都要經過的橋上。

 

走着走着,他非常的困,一不小心把手上的筆記本電腦掉到了河裏,他想都沒想,就從橋上跳了下去,要把筆記本電腦打撈起來。

 

這時,河神從河裏冒了出來,開口問這個程序員:“可愛的程序員喲,你下班這麼晚,你是忘掉了這一堆Java鎖呢?還是這一把死鎖呢?”

 

程序員一聽,非常高興,忙問河神:“您的意思是如果我說實話的話,你會把這些都放進我腦子裏面,對嗎?”

 

河神回答道:“不是的喲,我只是想告訴你,你腦子進水了喲!”

可愛的程序員喲,你忘掉的是這堆Java鎖呢?還是這把死鎖呢?

 

Java鎖

  • 樂觀鎖
  • 悲觀鎖
  • 自旋鎖
  • Synchronized 同步鎖
  • ReentrantLock
  • Semaphore 信號量
  • AtomicInteger
  • 可重入鎖(遞歸鎖)
  • 公平鎖與非公平鎖
  • ReadWriteLock 讀寫鎖
  • 共享鎖和獨佔鎖
  • 重量級鎖
  • 輕量級鎖
  • 偏向鎖
  • 分段鎖
  • 鎖優化

注:完整內容至後臺私信獲取免費領取方式!生活不易,感謝支持!!!

針對於Java程序員,我這邊還準備免費的Java架構學習資料(裏面有高可用、高併發、高性能及分佈式、Jvm性能調優、MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)

可愛的程序員喲,你忘掉的是這堆Java鎖呢?還是這把死鎖呢?

 

1、樂觀鎖

樂觀鎖是一種樂觀思想,即認爲讀多寫少,遇到併發寫的可能性低,每次去拿數據的時候都認爲別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,採取在寫時先讀出當前版本號,然後加鎖操作(比較跟上一次的版本號,如果一樣則更新),如果失敗則要重複讀-比較-寫的操作。

Java 中的樂觀鎖基本都是通過 CAS 操作實現的,CAS 是一種更新的原子操作,比較當前值跟傳入值是否一樣,一樣則更新,否則失敗


2、悲觀鎖

悲觀鎖是就是悲觀思想,即認爲寫多,遇到併發寫的可能性高,每次去拿數據的時候都認爲別人會修改,所以每次在讀寫數據的時候都會上鎖,這樣別人想讀寫這個數據就會 block 直到拿到鎖。java中的悲觀鎖就是Synchronized,AQS框架下的鎖則是先嚐試cas樂觀鎖去獲取鎖,獲取不到,纔會轉換爲悲觀鎖,如 RetreenLock。


3、自旋鎖

自旋鎖原理非常簡單,如果持有鎖的線程能在很短時間內釋放鎖資源,那麼那些等待競爭鎖的線程就不需要做內核態和用戶態之間的切換進入阻塞掛起狀態,它們只需要等一等(自旋),等持有鎖的線程釋放鎖後即可立即獲取鎖,這樣就避免用戶線程和內核的切換的消耗。

線程自旋是需要消耗 cup 的,說白了就是讓 cup 在做無用功,如果一直獲取不到鎖,那線程也不能一直佔用 cup 自旋做無用功,所以需要設定一個自旋等待的最大時間。

如果持有鎖的線程執行的時間超過自旋等待的最大時間扔沒有釋放鎖,就會導致其它爭用鎖的線程在最大等待時間內還是獲取不到鎖,這時爭用線程會停止自旋進入阻塞狀態。

 自旋鎖的優缺點

自旋鎖儘可能的減少線程的阻塞,這對於鎖的競爭不激烈,且佔用鎖時間非常短的代碼塊來說性能能大幅度的提升,因爲自旋的消耗會小於線程阻塞掛起再喚醒的操作的消耗,這些操作會導致線程發生兩次上下文切換!

 

但是如果鎖的競爭激烈,或者持有鎖的線程需要長時間佔用鎖執行同步塊,這時候就不適合使用自旋鎖了,因爲自旋鎖在獲取鎖前一直都是佔用 cpu 做無用功,佔着 XX 不 XX,同時有大量線程在競爭一個鎖,會導致獲取鎖的時間很長,線程自旋的消耗大於線程阻塞掛起操作的消耗,其它需要 cup 的線程又不能獲取到 cpu,造成 cpu 的浪費。所以這種情況下我們要關閉自旋鎖;

 自旋鎖時間閾值(1.6 引入了適應性自旋鎖)

自旋鎖的目的是爲了佔着 CPU 的資源不釋放,等到獲取到鎖立即進行處理。但是如何去選擇自旋的執行時間呢?如果自旋執行時間太長,會有大量的線程處於自旋狀態佔用 CPU 資源,進而會影響整體系統的性能。因此自旋的週期選的格外重要!

 

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

③ 自旋鎖的開啓

JDK1.6 中-XX:+UseSpinning 開啓;

 

-XX:PreBlockSpin=10 爲自旋次數;

 

JDK1.7 後,去掉此參數,由 jvm 控制;


4、Synchronized 同步鎖

synchronized 它可以把任意一個非 NULL 的對象當作鎖。他屬於獨佔式的悲觀鎖,同時屬於可重入鎖。

 Synchronized 作用範圍

1. 作用於方法時,鎖住的是對象的實例(this);

 

2. 當作用於靜態方法時,鎖住的是Class實例,又因爲Class的相關數據存儲在永久帶PermGen(jdk1.8 則是 metaspace),永久帶是全局共享的,因此靜態方法鎖相當於類的一個全局鎖,會鎖所有調用該方法的線程;

 

3. synchronized 作用於一個對象實例時,鎖住的是所有以該對象爲鎖的代碼塊。它有多個隊列,當多個線程一起訪問某個對象監視器的時候,對象監視器會將這些線程存儲在不同的容器中。

 Synchronized 核心組件

1) Wait Set:哪些調用 wait 方法被阻塞的線程被放置在這裏;

 

2) Contention List:競爭隊列,所有請求鎖的線程首先被放在這個競爭隊列中;

 

3) Entry List:Contention List 中那些有資格成爲候選資源的線程被移動到 Entry List 中;

 

4) OnDeck:任意時刻,最多隻有一個線程正在競爭鎖資源,該線程被成爲 OnDeck;

 

5) Owner:當前已經獲取到所資源的線程被稱爲 Owner;

 

6) !Owner:當前釋放鎖的線程。

③ Synchronized 實現

可愛的程序員喲,你忘掉的是這堆Java鎖呢?還是這把死鎖呢?

 

1. JVM 每次從隊列的尾部取出一個數據用於鎖競爭候選者(OnDeck),但是併發情況下,

ContentionList 會被大量的併發線程進行 CAS 訪問,爲了降低對尾部元素的競爭,JVM 會將

一部分線程移動到 EntryList 中作爲候選競爭線程。

 

2. Owner 線程會在 unlock 時,將 ContentionList 中的部分線程遷移到 EntryList 中,並指定

EntryList 中的某個線程爲 OnDeck 線程(一般是最先進去的那個線程)。

 

3. Owner 線程並不直接把鎖傳遞給 OnDeck 線程,而是把鎖競爭的權利交給 OnDeck,

OnDeck 需要重新競爭鎖。這樣雖然犧牲了一些公平性,但是能極大的提升系統的吞吐量,在

JVM 中,也把這種選擇行爲稱之爲“競爭切換”。

......

10. 鎖可以從偏向鎖升級到輕量級鎖,再升級到重量級鎖。這種升級過程叫做鎖膨脹;

 

11. JDK 1.6 中默認是開啓偏向鎖和輕量級鎖,可以通過-XX:-UseBiasedLocking 來禁用偏向鎖。


5、ReentrantLock

可愛的程序員喲,你忘掉的是這堆Java鎖呢?還是這把死鎖呢?

 

ReentantLock 繼承接口 Lock 並實現了接口中定義的方法,他是一種可重入鎖,除了能完成synchronized 所能完成的所有工作外,還提供了諸如可響應中斷鎖、可輪詢鎖請求、定時鎖等避免多線程死鎖的方法。

 Lock 接口的主要方法

1. void lock():執行此方法時,如果鎖處於空閒狀態,當前線程將獲取到鎖。相反,如果鎖已經被其他線程持有,將禁用當前線程,直到當前線程獲取到鎖。

 

2. boolean tryLock():如果鎖可用,則獲取鎖,並立即返回 true,否則返回 false。該方法和lock()的區別在於,tryLock()只是"試圖"獲取鎖,如果鎖不可用,不會導致當前線程被禁用當前線程仍然繼續往下執行代碼。而 lock()方法則是一定要獲取到鎖,如果鎖不可用,就一直等待,在未獲得鎖之前,當前線程並不繼續向下執行。

 

......

 

15. tryLock():嘗試獲得鎖,僅在調用時鎖未被線程佔用,獲得鎖。

 

16. tryLock(long timeout TimeUnit unit):如果鎖在給定等待時間內沒有被另一個線程保持,則獲取該鎖。

 非公平鎖

JVM 按隨機、就近原則分配鎖的機制則稱爲不公平鎖,ReentrantLock 在構造函數中提供了是否公平鎖的初始化方式,默認爲非公平鎖。非公平鎖實際執行的效率要遠遠超出公平鎖,除非程序有特殊需要,否則最常用非公平鎖的分配機制。

 公平鎖

公平鎖指的是鎖的分配機制是公平的,通常先對鎖提出獲取請求的線程會先被分配到鎖,ReentrantLock 在構造函數中提供了是否公平鎖的初始化方式來定義公平鎖。

 ReentrantLock 與 synchronized

1. ReentrantLock 通過方法 lock()與 unlock()來進行加鎖與解鎖操作,與 synchronized 會被 JVM 自動解鎖機制不同,ReentrantLock 加鎖後需要手動進行解鎖。爲了避免程序出現異常而無法正常解鎖的情況,使用 ReentrantLock 必須在 finally 控制塊中進行解鎖操作。

 

2. ReentrantLock 相比 synchronized 的優勢是可中斷、公平鎖、多個鎖。這種情況下需要使用ReentrantLock。

⑤ ReentrantLock 實現

可愛的程序員喲,你忘掉的是這堆Java鎖呢?還是這把死鎖呢?

 

 Condition 類和 Object 類鎖方法區別區別

 tryLock 和 lock 和 lockInterruptibly 的區別

可愛的程序員喲,你忘掉的是這堆Java鎖呢?還是這把死鎖呢?

 

WC,還有好多

全部分享出來的話,篇幅很長,會給大家增加閱讀負擔,全部的內容,到後臺私信領取吧!

還有...

可愛的程序員喲,你忘掉的是這堆Java鎖呢?還是這把死鎖呢?

 

可愛的程序員喲,你忘掉的是這堆Java鎖呢?還是這把死鎖呢?

 

還有...

下篇文章將繼續分享Java多線程編髮:上下文切換、線程池原理等內容,關注我上車別跟丟!

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