?一、💛
鎖策略——接上一篇
6.分為可重入鎖,不可重入鎖
如果一個線程,針對一把鎖,連續加鎖兩次,會出現死鎖,就是不可重入鎖,不會出現死鎖,就是可重入鎖。
如果一個線程,針對一把鎖,連續加鎖兩次,如果產生了死鎖,就是不可重入鎖😄
public class Demo5 {public static void main(String[] args) {Thread t=new Thread(new Runnable() {int count=0;@Overridepublic synchronized void run() { //加了一層鎖synchronized (this){ //加了第二次鎖count++;}System.out.println(count);}});t.start();} }
那么我們來解釋一下什么叫做死鎖呢?
public synchronized void run() { ? ?//加了一層鎖
? ? ? ? ? ? ? ? synchronized (this){ ? ? ? ? ? ?//加了第二次鎖
? ? ? ? ? ? ? ? ? ? count++;
? ? ? ? ? ? ? ? }這個代碼中,調用方法先針對this加鎖,此時假設加鎖成功了,接下來到往下執行代碼塊中的this來進行加鎖,此時就會出現鎖競爭,this已經處于鎖狀態了,此時該線程就會阻塞~一直阻塞到鎖被釋放,才能有機會拿到鎖。
這也是死鎖第一個體現:this這個鎖必須要run執行完畢,才能釋放,但是要想執行完事,這個第二次加鎖就應該加上,方法才可以執行,但是第二次想加上第一個就應該放鎖,所以由于this鎖沒法釋放,代碼就卡在這里了,因此線程數量就僵住了。
還好synchronized是可重入鎖,JVM幫我們承擔了很多的任務
這里卡死就很不科學的一種情況,第二次嘗試加鎖的時候,該線程已經有了這個鎖的權限了~~這個時候,不應該加鎖失敗,不應該進行阻塞等待的~
不可重入鎖:這把鎖不會保存,哪個線程加上的鎖,只要他當前處于加鎖狀態之后,收到了‘加鎖的請求’,就會拒絕當前加鎖,而不管當下線程是哪個,就會產生死鎖。?🌝
可重入鎖:會讓這個鎖保存,是哪個線程加的鎖,后續收到加鎖請求之后,就會先對比一下,看看加鎖的線程是不是當前持有自己這把鎖的線程~~這個時候就可以靈活判定了。
那么該如何對比捏🌚
synchronized(this){synchronized(this){synchronized(this){ ······->執行到這個代碼,出了這個代碼,剛才加上的鎖,是否要釋放?} 如果最里面的釋放了鎖,意味著最外面的synchronized和中間的synchronized后 } 續的代碼部分就沒有處在鎖的保護之中了
真正要在這個地方釋放鎖,如加鎖N層遇到了 } , JVM如何知道是最后一個呢,整一個整型變量,記錄當前這個線程加了幾次鎖,每遇到一個加鎖操作,計數器+1,每遇到一個解鎖操作,就-1,當計數器減為0時,才真正執行釋放鎖操作,其他時候時不釋放的。這一個思想就叫做‘引用計數’🐲🐲🐲(腦力+10000,人類進化不帶我)
注補充:靜態方法是針對類加鎖,普通方法是針對this加鎖
二、💙
死鎖的詳細介紹:兩次加鎖,都是同一個線程
死鎖的三種典型情況;
1.一個線程,一把鎖,但是不可入鎖,該線程針對這個鎖聯系加兩次就會出現死鎖。
2.兩個鎖,兩個鎖,這兩個縣層先分別獲取一把鎖,然后再嘗試分別獲取對方的鎖(
比如我拿了醬油要炫餃子,小楊拿醋,我讓他把醋先給我,然后我一起給你,小楊一拍桌子,憑啥先給你,你多個啥?),如下圖,雙方陷入死循環中
?
public class Demo5 {public static Object locker1=new Object();public static Object locker2=new Object();public static void main(String[] args) {Thread t1=new Thread(new Runnable() {@Overridepublic synchronized void run() {synchronized (locker1) { //給1加鎖System.out.println("s1.start");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker2) { //沒有放棄1的鎖System.out.println("s2.over");}}}});t1.start();Thread t2=new Thread(new Runnable() {@Overridepublic synchronized void run() {synchronized (locker2) { //給2加鎖System.out.println("t2.start");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker1) { //沒有放棄2的鎖System.out.println("t1.over");}}}});t2.start();} }
3.N個線程,M把鎖:
(哲學家就餐問題)
?三、💜
如何應該避免死鎖呢?先明確死鎖產生的原因,死鎖的必要條件(缺一不可)
1.互斥使用:一個線程獲取到一把鎖之后,別的線程不能獲取到這個鎖。(實際使用的鎖,一般都是互斥的鎖的基本特性)
2.不可搶占:鎖只能被持有者主動釋放,而不能是其他線程直接搶走(也是鎖的基本特性)
3.請求和保持:這一個線程嘗試獲取多把鎖,在獲取第二把時候,會保持對第一把的獲取狀態(取決于代碼結構)比如剛才寫的,我只要讓他獲取完第一把再釋放,在獲取第二把,這樣不發生沖突,但是可能會影響需求。
4.循環等待:t1嘗試獲取locker2,需要t2執行完釋放locker2,t2嘗試獲取locker1,需要t1執行完畢,釋放locker1(取決于代碼結構)
我們的解決方式趨向于解決第四種(打破循環等待,如何具體實現解決死鎖,實際方法有很多)
首先就是銀行家算法(殺牛刀了屬于,復雜,沒必要會)
簡單有效方法:針對鎖進行編號,且規定加鎖的順序,只要線程加鎖的順訊,都嚴格執行上述順序,就沒有循環等待。
如下:
一般面試我們主動點: ?問到死鎖撿著了,細細的答,給他講,讓他覺得你是理解的
1.什么是死鎖。 ? ? ? ? ??
2.死鎖的幾個典型場景
3.死鎖產生的必要條件
4.如何解決死鎖的問題
?
四、???
synchronized具體采用了哪些鎖策略呢?
1.既是悲觀鎖,又是樂觀鎖
2.既是重量級鎖,又是輕量級鎖
3.重量級鎖部分是基于多系統互斥鎖實現的,輕量級鎖部分是基于自旋鎖實現的
4.synchronized是非公平鎖(不會遵守先來后到,鎖釋放之后,哪個線程拿到鎖個憑本事
5.synchronized是可重入鎖(內部會記錄哪個線程拿到了鎖,記錄引用計數)
6.synchronized不是讀寫鎖
synchronized-內部實現策略(自適應)
講解一下自適應:代碼中寫了一個synchhronized之后,可能產生一系列自適應的過程,鎖升級(鎖膨脹)
無鎖->偏向鎖->輕量級鎖->重量級鎖
偏向鎖,不是真的加鎖,而只是做了一個標記,如果有別的線程來競爭鎖,才會真的加鎖,如果沒有別的線程競爭,就自始至終都不加鎖了(渣女心態,沒人來追你,我就釣魚,你要是被追了,我先給你個身份,讓別人別靠近你。)——當然加鎖本身也有一定消耗
偏向鎖在沒人競爭的時候就是一個簡單的(輕量的)標記,如果有別的線程來嘗試加鎖,就立即把偏向鎖升級成真正加鎖,讓別人阻塞等待(能不加鎖就不加鎖)
輕量級鎖-synchronized通過自旋鎖的方式實現輕量級鎖——這邊把鎖占據了,另一個線程按照自旋的方式(這個鎖操作比較耗cpu,如果能夠快速拿到鎖,多耗點也不虧),來反復查詢當前的鎖狀態是不是被釋放,但是后續,如果競爭這把鎖的線程越來越多了(鎖沖突更加激烈了),從輕量鎖,升級到重量級鎖~隨著競爭激烈,即使前一個線程釋放鎖,也不一定能夠拿到鎖,何時能拿到,時間可能比較久了會
?💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖
鎖清除:編譯器,會智能的判斷,當前這個代碼,是否有必要加鎖,如果你寫了加鎖,但實際沒必要加鎖,就會自動清除鎖
如:單個線程使用StringBuffer編譯器進行優化,是保證優化之后的邏輯和之前的邏輯是一致的,這樣就會讓代碼優化變的保守起來~~咱們猿們也不能指望編譯器優化,來提升代碼效率,自己也要有作用,判斷何時加鎖,也是咱們非常重要的工作。
鎖粗化:
關于鎖的粒度,鎖中操作包含代碼多:鎖粒就大
//1號 全寫的是偽代碼 和2號比較明顯是2號的粒度更大 for( synchronized(this){count++} }//2號 synchronized(this){ for{ count++} }
鎖粒大,鎖粒小各有好處:
鎖粒小,并發程度會更高,效率也會更快
鎖粒大,是因為加鎖本身就有開銷。(如同打電話,打一次就行,老打電話也不好)
上述的都是基本面試題
五、💚
CAS全稱(Compare and swap) 字面意思:比較并且交換
能夠比較和交換,某個寄存器中的值和內存中的值,看是否相等,如果相等就把另一個寄存器中的值和內存進行交換
boolean CAS(address,expectValue,swapValue){if(&address==expectValue){ //這個&相當于C語言中的*,看他兩個是否相等&address=swapValue; //相等就換值return true; }return false;
此處嚴格的說是,adress內存的值和swapValue寄存器里的值,進行交換,但是一般我們重點關注的是內存中的值,寄存器往往作為保存臨時數據的方式,這里的值是啥,很多時候我們選擇是忽略的。
這一段邏輯是通過一條cpu指令完成的(原子的,或者說確保原子性)給我們編寫線程安全代碼,打開了新的世界。
CAS的使用
1.實現原子類:多線程針對一個count++,在java庫中,已經提供了一組原子類
java.util.concurrent(并發的意思).atomic
AtomicInteger,AtomicLong,提供了自增/自減/自增任意值,自減任意值··,這些操作可以基于CAS按照無鎖編程的方式來實現。
如:for(int i=0;i<5000;i++){
count.getAndIncrement(); ? ? ? ? ? ? ? ? ? ? ? ? //count++
count.incrementAndGet(); ? ? ? ? ? ? ? ? ? ? ? ?//++count
count.getAndDecrement(); ? ? ? ? ? ? ? ? ? ? ?//count--
count.decrementAndGet() ? ? ? ? ? ? ? ? ? ? ?//--count
}
import java.util.concurrent.atomic.AtomicInteger;public class Demo6 {public static AtomicInteger count=new AtomicInteger(0); //這個類的初值唄public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(()->{for (int i=0;i<500;i++){count.getAndIncrement();}});Thread t2=new Thread(()->{for (int i=0;i<500;i++){count.getAndIncrement();}});t1.start();t2.start();t1.join(); //注意要等待兩個線程都結束再開始調用t2.join();System.out.println(count);} }
上述原子類就是基于CAS完成的
當兩個線程并發的線程執行++的時候,如果加限制,意味著這兩個++是串行的,能計算正確的,有時候者兩個++操作是穿插的,這個時候是會出現問題的
加鎖保證線程安全:通過鎖,強制避免出現穿插~~
原子類/CAS保證線程安全,借助CAS來識別當前是否出現穿插的情況,如果沒有穿插,此時直接修改就是安全的,如果出現了穿插,就會重新讀取內存中最新的值,再次嘗試修改。
部分源碼合起來的意思就是?
public int getAndIncrement(){int oldValue=value; //先儲存值,防止別的線程偷摸修改之后,無法恢復到之前的值while(CAS(Value,oldValue,OldValue+1)!=true){ //檢查是否線程被別的偷摸修改了//上面的代碼是Value是否等于oldValue,假如等于就把Value賦值OldValue+1oldValue=value; //假如修改了就恢復了原來的樣子}return oldValue;}
?
假如這種情況,剛開始設置value=0,?
CAS是一個指令,這個指令本身是不能夠拆分的。
是否可能會出現,兩個線程,同時在兩個cpu上?微觀上并行的方式來執行,CAS本身是一個單個的指令,這里其實包含了訪問操作,當多個cpu嘗試訪問內存的時候,本質也是會存在先后順序的。
就算同時執行到CAS指令,也一定有一個線程的CAS先訪問到內存,另一個后訪問到內存
為啥CAS訪問內存會有先后呢?
多個CPU在操作同一個資源,也會涉及到鎖競爭(指令級別的鎖),是比我們平時說的synchronized代碼級別的鎖要輕量很多(cpu內部實現的機制)?