可重入鎖VS不可重入鎖
有一個線程,針對同一把鎖,連續加鎖兩次,如果產生了死鎖,那就是不可重入鎖,如果沒有產生死鎖,那就是可重入鎖.
死鎖
我們之前引入多線程的時候不是講了一個加數字的案例么,我們今天以它來舉例
當我們這樣寫的時候會出現什么問題?
分析:第一個synchronized的加鎖對象是 this ,當我們繼續執行代碼,就會發現第二個synchronized的加鎖對象還是 this ,此時就需要注意,當一個對象已經被加鎖了,此時嘗試對這個已經加鎖的對象再一次的進行加鎖,就會出現"鎖競爭",我們要想使得第二個synchronized實現對 this 的加鎖,就要讓increase執行完畢,但是要想讓increase執行完畢,就需要第二個synchronized加鎖成功,此時就陷入了循環,就出現了矛盾.此時這個代碼就卡在這里了,因此這個線程就僵住了.這是死鎖的第一個體現形式.
這里的關鍵在于,兩次加鎖,都是"同一個線程",第二次嘗試加鎖的時候,該線程已經有了這個鎖的權限了,這個時候,不應該加鎖失敗的,不應該阻塞等待的.
如果是一個不可重入鎖,這把鎖不會保存,是哪個線程對它加的鎖,只要它當前處于加鎖狀態之后,收到了"加鎖"這樣的請求,就會拒絕當前加鎖,而不管當下的線程是哪個,就會產生死鎖.
如果是一個可重入鎖,則是會讓這個鎖保存,是哪個線程加上的鎖,后續收到加鎖請求之后,就會先對比一下,看看加鎖的線程是不是當前自己持有這把鎖的線程,這個時候就可以靈活判定了.
但慶幸的是,synchronized本身就是一個可重入鎖,實際上我們上述的那個舉例子的代碼并不會出現死鎖的情況.
首先,答案是肯定的,不能釋放,如果在這里釋放鎖了,那么中間的synchronized以及中間的代碼就不會受到鎖的保護了.
那么我們想一下,可重入鎖,需要比不可重入鎖額外多出哪些功能
1.判斷當前加鎖的線程是不是同一個線程
2.判斷當前代碼執行到的是第幾層的鎖,那么這個功能是怎么實現的呢?
其實很簡單,持有一個"計數器"就可以了,讓鎖對象不光要記錄是哪個線程持有鎖,同時再通過一個整形變量記錄當前這個線程加了幾次鎖,每遇到一個加鎖操作,就計數器+1,每遇到一個解鎖操作就-1,當計數器減為0的時候,才真正執行釋放鎖的操作,其他時候不釋放鎖.而類似于這個操作的操作,我們稱它為"引用計數"
死鎖的三種典型情況
1.一個線程,一把鎖,但是是不可重入鎖,該線程針對這個鎖連續加鎖兩次,就會出現死鎖
2.兩個線程,兩把鎖,這兩個線程先分別獲取到一把鎖,然后再同時嘗試獲取對方的鎖
下面我們敲代碼來理解一下死鎖
首先,這是一個錯誤的代碼
執行結果如下
實際上由剛才的分析可以知道,這里會出現死鎖的情況,那么為什么這里和理論值不一樣呢?
因為沒有加Sleep
代碼如上
執行結果如下
那么,為什么?為什么加了Sleep之后會不一樣,究竟是Sleep改變了線程原有的樣子,還是說Sleep恢復了線程原有的樣子?
答案是后者
我們在使用多線程的時候會發現,程序運行的時間特別長了會經常出現一些問題,或者說當我們來氣了多個線程他們分別執行幾個任務,但是因為執行的任務的時間非常短,有時候CPU切換的時候會出現一系列的問題.
原因是當我們設置Sleep是,就等于告訴CPU,當前的線程不再運行,持有當前對象的鎖.那么這個時候CPU就會切換到另外的線程了,這種操作在有些時候是非常好的.
3.N個線程M把鎖
哲學家就餐問題
每個哲學家,主要做兩件事
1.思考人生,會放下筷子
2.吃面.會拿起左手和右手的筷子
3.每個哲學家,什么時候思考人生,什么時候吃面條,都不好說
2.每個哲學家一旦想吃面條了,就會非常固執的完成吃面條的這個操作,如果此時,他的筷子被別人使用了,就會阻塞等待,而且等待的過程中不會放下手中已經拿著的的筷子
是否有辦法去避免死鎖呢?
先明確產生死鎖的原因,死鎖的必要條件
四個必要條件(缺一不可,只要破壞其中任意一個條件,就可以避免死鎖)
1.互斥使用,一個線程獲取到一把鎖之后,別的線程不能獲取到這個鎖(我們實際使用的鎖,一般都是互斥的(鎖的基本特性))
2.不可搶占,鎖只能是被持有者主動釋放,而不是被其他線程直接搶走(鎖的基本特性)
3.請求和保持,這一個線程嘗試去獲取多把鎖,在獲取第二把鎖的過程中,會保持對第一把鎖的獲取狀態(取決于代碼結構)(很可能會影響到需求)
在獲取第二把鎖的同時會保持對第一把鎖的狀態,這里由于獲取第二把鎖的時候,并沒有去釋放第一把鎖,所以就會出現阻塞等待
當我們將代碼改成這樣,即獲取完第一把鎖之后,并且將第一把鎖釋放掉,此時再去請求獲取第二把鎖,這樣做是不會出現死鎖的
4.循環等待.t1嘗試獲取 locker2,需要t2執行完,釋放locker2;t2嘗試獲取locker1,需要t1執行完,釋放locker1 (取決于代碼結構)(解決死鎖問題的最關鍵要點)
如果具體解決死鎖問題,實際的方法有很多種(例:銀行家算法(但不推薦,因為不接地氣))
介紹一個更簡單,也非常有效的解決死鎖的辦法
針對鎖進行編號,并且規定加鎖的順序
比如,約定,每個線程如果要獲取多把鎖,必須先獲取編號小的鎖,后獲取編號大的鎖.只要所有線程加鎖的順序,都嚴格遵守上述順序,就一定不會出現循環等待.
像這樣,我們規定先讓locker1加鎖,然后讓locker2加鎖,按照指定順序加鎖,也就可以避免死鎖的問題了
synchronized具體是采用了哪些鎖策略
1.synchronized即是悲觀鎖,也是樂觀鎖.
2.synchronized即是重量級鎖,也是輕量級鎖.
3.synchronized重量級鎖部分是基于系統的互斥鎖實現的,輕量級鎖部分是基于自旋鎖實現的.
4.synchronized是非公平鎖(不會遵守先來后到,鎖釋放了之后,哪個線程拿到鎖,各憑本事).
5.synchronized是可重入鎖(內部會記錄哪個線程拿到了鎖,記錄引用次數).
6.synchronized不是讀寫鎖.
synchronized內部實現策略(內部原理)
代碼中寫了一個synchronized之后,這里可能會產生一系列的"自適應的過程",鎖升級(鎖膨脹)
無鎖->偏向鎖->輕量級鎖->重量級鎖
偏向鎖(懶漢模式思想的延伸)
不是真的加鎖,而只是做了一個"標記".如果有別的線程來競爭鎖了,才會真的加鎖,如果沒有別的線程競爭,就自始至終都不會真的加鎖了.(加鎖本身,有一定的開銷,能不加就不加,非得是有人來競爭了,才會真的加鎖)
偏向鎖在沒有其他人競爭的時候,就僅僅是一個簡單的標記(非常輕量).一旦有別的線程嘗試加鎖,就會立刻把偏向鎖升級為一個真正的加鎖狀態,讓其他線程只能阻塞等待
輕量級鎖
synchronized通過自旋的方式來實現輕量級鎖,我這邊把鎖占據了,另一個線程就會按照自旋的方式,來反復查詢當前的鎖的狀態是不是被釋放了.但是,后續如果競爭這把鎖的線程越來越多了(鎖沖突更激烈了),就會從輕量級鎖,升級成重量級鎖
鎖消除
編譯器,會智能的判定,當前的這個代碼,是否有必要加鎖,如果你寫了加鎖,但實際上沒有必要加鎖,就會把加鎖操作自動優化掉。
比如在單個線程中使用StringBuffer.
編譯器進行優化,是要保證優化之后的邏輯和之前的邏輯是一致的
鎖粗化
關于"鎖的粒度"如果加鎖操作里包含的實際要執行的代碼越多,就認為鎖的粒度越大.
//以下是一些偽代碼
//鎖的粒度小
for(.....){synchronized(this){count++; }
}
//鎖的粒度大
synchronized(this){for(.....){count++; }
}
?