給我一首歌的時間,帶你瞭解執行緒安全和死鎖(2)

語言: CN / TW / HK

持續創作,加速成長!這是我參與「掘金日新計劃 · 10 月更文挑戰」的第12天,點選檢視活動詳情

大家好,我是bug郭,一名雙非科班的在校大學生。對C/JAVA、資料結構、Spring系列框架、Linux及MySql、演算法等領域感興趣,喜歡將所學知識寫成部落格記錄下來。 希望該文章對你有所幫助!如果有錯誤請大佬們指正!共同學習交流

作者簡介:

執行緒不安全原因

  • 執行緒是搶佔性執行的,執行緒間的排程充滿了隨機性(執行緒不安全的根本原因)
  • 多個執行緒對同一個資源進行了更改操作(如果是不同的資源,或者只是對一個資源進行讀操作就不會出現執行緒不安全問題) 我們可以更改程式碼結構,讓不同的執行緒對不同的變數進行更改就不會出現問題
  • 針對變數的操作不是原子的! 因為對變數的操作的指令並不是一條!而是多條指令! 我們可以通過加鎖操作,使多個指令打包成一個原子,避免執行緒不安全問題!
  • 記憶體可見性 什麼是記憶體可見性呢? 就是編譯器對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根筷子! 顯然筷子不夠!但是就將用! 每個哲學家的兩邊都分別有一根筷子!

情形如下: 在這裡插入圖片描述 而且哲學家都比較倔,當他們拿到一根筷子時,如果沒有筷子了,他們會一直等待,直到又一雙筷子,才會乾飯!

如果哲學家同時拿一根筷子時,就會造成死鎖! 這頓飯估計永遠都結束不了!!!

如何解決上面的問題呢? 我們可以先個筷子編個號,然後讓哲學家們約定好,先拿編號小的筷子,再拿編號大的筷子,如果編號小的筷子被拿了,那就一直等待! 在這裡插入圖片描述 這樣一約定,哲學家就可以將這頓飯幹完了!