給我一首歌的時間,帶你瞭解執行緒安全和死鎖(2)
持續創作,加速成長!這是我參與「掘金日新計劃 · 10 月更文挑戰」的第12天,點選檢視活動詳情
大家好,我是bug郭,一名雙非科班的在校大學生。對C/JAVA、資料結構、Spring系列框架、Linux及MySql、演算法等領域感興趣,喜歡將所學知識寫成部落格記錄下來。 希望該文章對你有所幫助!如果有錯誤請大佬們指正!共同學習交流
作者簡介:
- CSDN java領域新星創作者blog.csdn.net/bug..
- 掘金LV3使用者 juejin.cn/user/bug..
- 阿里雲社群專家博主,星級博主,developer.aliyun.com/bug..
- 華為云云享專家 bbs.huaweicloud.com/bug..
執行緒不安全原因
- 執行緒是搶佔性執行的,執行緒間的排程充滿了隨機性(執行緒不安全的根本原因)
- 多個執行緒對同一個資源進行了更改操作(如果是不同的資源,或者只是對一個資源進行讀操作就不會出現執行緒不安全問題) 我們可以更改程式碼結構,讓不同的執行緒對不同的變數進行更改就不會出現問題
- 針對變數的操作不是原子的! 因為對變數的操作的指令並不是一條!而是多條指令! 我們可以通過加鎖操作,使多個指令打包成一個原子,避免執行緒不安全問題!
-
記憶體可見性 什麼是記憶體可見性呢? 就是編譯器對
cpu
操作的優化! 舉個簡單的例子: 當一個執行緒一直迴圈讀一個數據時,我們的cpu
就要一直在記憶體中讀資料,而我們知道記憶體讀取的速度想必於cpu
中的暫存器慢了好幾個數量級!那麼這時編譯器就進行優化,他懶得去記憶體中讀取資料了,直接將資料儲存在暫存器進行讀取操作,而如果這時有另外一個執行緒對該資料進行修改,那麼就會產生執行緒不安全問題!!! 我們的編譯器都是由大佬編寫的,所以在不改變邏輯性和結果的情況下,會對程式碼進行優化!!! 如何避免該問題呢? 我們可以採用synchronized
關鍵字對執行緒加鎖或者使用volatile
關鍵字保證記憶體可見性! -
指令重排序導致執行緒不安全問題,這也是由於編譯器的優化操作而導致的執行緒不安全問題! 我們的程式碼先後執行順序有時候並不會影響我們的結果,那麼這時編譯器在不改變程式碼邏輯的基礎上就會改變一下順序,提高執行效率,而這個操作在多執行緒往往會出現執行緒不安全問題! 這裡也可以使用
synchronized
關鍵字避免指令重排列!
synchronized
關鍵字
我們已經瞭解到了執行緒不安全問題可以用java
提供的synchronized
關鍵字來避免!
我們來學習synchronized
如何使用!
-
修飾例項方法
java class Count{ int count=0; //對例項方法進行加鎖 synchronized public void increase(){ count++; } }
這裡的加速操作就是相當於對該例項的物件(this
)進行加鎖,而程式碼底層又是如何完成這個加鎖操作的呢? 我們知道一切物件的父類都是Object
類,而我們建立一個類,除了有我們描述的基本屬性外,java
還會自動開闢一塊空間儲存物件頭
資訊!顯然我們沒有聽說過,這裡的屬性是給jvm
使用的!我們程式設計師並沒有用!而加鎖就是在物件頭
設定一個鎖的標誌位! 如果多個執行緒對同一個鎖進行操作就會有鎖競爭 對不同的鎖進行操作就不會出現鎖競爭! -
修飾程式碼塊 java可以在任意位置加鎖!,但修飾程式碼塊時我們需要指明加鎖的物件!
java class Count{ int count=0; public void increase(){ synchronized(this){//指明加鎖物件! count++; } } }
- 修飾靜態方法
java
class Count{
static int count=0;
public static void increase(){
synchronized(Count.class){//修飾靜態方法!
count++;
}
}
}
當synchronozed
修飾靜態方法時,並不能對this
加鎖,因為這是類方法!
我們可以採用反射的反射對類物件進行加鎖!!!
死鎖
死鎖型別
- 一個執行緒一把鎖
我們知道
synchronized
關鍵字可以給物件加鎖!那如果我們不小心給同一個物件加鎖兩次,會出現什麼情況呢?
java
class Count_1{
private int count=0;
synchronized public void increase(){ //外層鎖
synchronized (this){ //內層鎖
count++;
}
}
}
假設synchronized
是不可重入鎖,就是不能進行多次加鎖!
我們的外層鎖,在進入方法後就會對該物件進行加鎖,有效加鎖!而裡層鎖,會一直阻塞等待外層鎖釋放鎖,才會進行加鎖,此時程式碼就阻塞在這裡了! 而外層鎖要方法執行結束,才能釋放鎖!
顯然現在的情形是誰也不讓著誰!這就導致了一個尷尬的局面,就是我們所說的死鎖
!
就好比生活中的例子:
你手機沒電了,要先老闆借個充電寶!老闆說,你先付錢我就借你,而你的手機已經關機了,又沒帶現金,然後你說,你借我我就付錢!然後就兩個阿叉棍在哪死鎖了!!!
不過我們寫jvm
的大佬設計時,將synchronized
設定成了可重入鎖!
多次加鎖並不會發生死鎖!
加鎖: 如果我們現在有一個執行緒
t1
,物件a
加鎖後,t1
執行緒拿到了該鎖!synchronized
就會在鎖資訊中記入該執行緒資訊,還有標誌該執行緒的加鎖次數為1,如果t1
執行緒再次對a
加鎖,那麼並不會真正的再次加鎖,只會把加鎖次數加一! 解鎖:如果該執行緒解鎖,鎖資訊就會將鎖次數減一,直到鎖次數為0,此時該執行緒就將該鎖釋放!
顯然jvm
這樣可重入鎖設定,如果我們多次對一個物件加鎖,會我們的執行速度降低,但是這樣提高了我們人力成本,如果為不可重入鎖,但造成死鎖,那該程式就會中斷,我們需要花大量時間進行除錯!!!
-
兩個執行緒兩把鎖 假設一種情況: 當有兩個人手機需要充電,而一個人有充電頭,一個人有資料線,然後想要充電的話,需要兩者結合,然後兩個人都比較倔,誰都不肯退讓,這就造成了死鎖!
-
N個執行緒M把鎖 這裡就需要講到教科書上的經典案例:哲學家就餐問題 假如有一群哲學家圍在一個圓桌上乾飯,然後他們乾飯的時候還會思考人生,思考人生時不那筷子吃飯! 他們吃飯和思考人生是隨機的! 假如有5個人,5根筷子! 顯然筷子不夠!但是就將用! 每個哲學家的兩邊都分別有一根筷子!
情形如下: 而且哲學家都比較倔,當他們拿到一根筷子時,如果沒有筷子了,他們會一直等待,直到又一雙筷子,才會乾飯!
如果哲學家同時拿一根筷子時,就會造成死鎖! 這頓飯估計永遠都結束不了!!!
如何解決上面的問題呢? 我們可以先個筷子編個號,然後讓哲學家們約定好,先拿編號小的筷子,再拿編號大的筷子,如果編號小的筷子被拿了,那就一直等待! 這樣一約定,哲學家就可以將這頓飯幹完了!