Java多線程 各種鎖(一篇全搞懂)

Java多線程 鎖

1、樂觀鎖與悲觀鎖

(1)悲觀鎖

對於同一個數據的併發操作,悲觀鎖認爲自己在使用數據的時候一定有別的線程來修改數據,因此在獲取數據的時候會先加鎖,確保數據不會被別的線程修改。Java中,synchronized關鍵字和Lock的實現類都是悲觀鎖。

(2)樂觀鎖

樂觀鎖認爲自己在使用數據時不會有別的線程修改數據,所以不會添加鎖,只是在更新數據的時候去判斷之前有沒有別的線程更新了這個數據。如果這個數據沒有被更新,當前線程將自己修改的數據成功寫入。如果數據已經被其他線程更新,則根據不同的實現方式執行不同的操作(例如報錯或者自動重試)。
樂觀鎖在Java中是通過使用無鎖編程來實現,最常採用的是CAS算法,Java原子類中的遞增操作就通過CAS自旋實現的
在這裏插入圖片描述
CSA算法詳解以及他們的使用場景和優缺點:
上一篇博客:Java多線程 樂觀鎖、悲觀鎖、以及CAS算法

2、公平鎖與非公平鎖

(1)公平鎖
公平鎖是指多個線程按照申請鎖的順序來獲取鎖,線程直接進入隊列中排隊,隊列中的第一個線程才能獲得鎖

優點:等待鎖的線程不會餓死
缺點:整體吞吐效率相對非公平鎖要低,等待隊列中除第一個線程以外的所有線程都會阻塞,CPU喚醒阻塞線程的開銷比非公平鎖大。

圖解:
在這裏插入圖片描述
(2)非公平鎖
非公平鎖是多個線程加鎖時直接嘗試獲取鎖,獲取不到纔會到等待隊列的隊尾等待。但如果此時鎖剛好可用,那麼這個線程可以無需阻塞直接獲取到鎖,所以非公平鎖有可能出現後申請鎖的線程先獲取鎖的場景

優點:可以減少喚起線程的開銷,整體的吞吐效率高,因爲線程有機率不阻塞直接獲得鎖,CPU不必喚醒所有線程

缺點:處於等待隊列中的線程可能會餓死,或者等很久纔會獲得鎖

圖解:
在這裏插入圖片描述

3、可重入鎖與不可重入鎖

(1)可重入鎖
可重入鎖又名遞歸鎖,是指在同一個線程在外層方法獲取鎖的時候,再進入該線程的內層方法會自動獲取鎖(前提鎖對象得是同一個對象或者class),不會因爲之前已經獲取過還沒釋放而阻塞。Java中ReentrantLock和synchronized都是可重入鎖,可重入鎖的一個優點是可一定程度避免死鎖。

(2)是指在同一個線程在外層方法獲取鎖的時候,不能再進入該線程的內層方法會自動獲取鎖。如(非可重入鎖NonReentrantLock)。

ReentrantLock和NonReentrantLock的實現原理:
其父類AQS中維護了一個同步狀態status來計數重入次數,status初始值爲0。當線程嘗試獲取鎖時,可重入鎖先嚐試獲取並更新status值,如果status == 0表示沒有其他線程在執行同步代碼,則把status置爲1,當前線程開始執行。如果status != 0,則判斷當前線程是否是獲取到這個鎖的線程,如果是的話執行status+1,且當前線程可以再次獲取鎖而非可重入鎖是直接去獲取並嘗試更新當前status的值,如果status != 0的話會導致其獲取鎖失敗,當前線程阻塞。

synchronized 實現是由jvm底層實現。

4、獨享鎖與共享鎖

(1)獨享鎖
獨享鎖也叫排他鎖,是指該鎖一次只能被一個線程所持有。如果線程T對數據A加上排它鎖後,則其他線程不能再對A加任何類型的鎖。獲得排它鎖的線程即能讀數據又能修改數據。JDK中的synchronized和JUC中Lock的實現類就是互斥鎖。(例如ReentrantReadWriteLock中的寫鎖)

(2)共享鎖
共享鎖是指該鎖可被多個線程所持有。如果線程T對數據A加上共享鎖後,則其他線程只能對A再加共享鎖,不能加排它鎖。獲得共享鎖的線程只能讀數據,不能修改數據。(例如ReentrantReadWriteLock中的讀鎖)

Java提供了一個基於AQS到讀寫鎖實現ReentrantReadWriteLock,該讀寫鎖到實現原理是:將同步變量state按照高16位和低16位進行拆分,高16位表示讀鎖,低16位表示寫鎖。
結構如下圖:

1、寫鎖是一個獨享鎖,所以我們看一下ReentrantReadWriteLock中tryAcquire(arg)的實現:

protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
            if (c != 0) {
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire
                setState(c + acquires);
                return true;
            }
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }
 }

獲取:
(1)獲取同步狀態,並從中分離出低16爲的寫鎖狀態
(2)如果同步狀態不爲0,說明存在讀鎖或寫鎖
(3)如果存在讀鎖(c !=0 && w == 0),則不能獲取寫鎖(保證寫對讀的可見性)
(4)如果當前線程不是上次獲取寫鎖的線程,則不能獲取寫鎖(寫鎖爲獨佔鎖)
(5)如果以上判斷均通過,則在低16爲寫鎖同步狀態上利用CAS進行修改(增加寫鎖同步狀態,實現可重入)
(6)將當前線程設置爲寫鎖的獲取線程

釋放的過程與獨佔鎖基本相同。

2、讀鎖是一個共享鎖,獲取讀鎖的步驟如下:

(1)獲取當前同步狀態
(2)計算高16爲讀鎖狀態+1後的值
(3)如果大於能夠獲取到的讀鎖的最大值,則拋出異常
(4)如果存在寫鎖並且當前線程不是寫鎖的獲取者,則獲取讀鎖失敗
(5)如果上述判斷都通過,則利用CAS重新設置讀鎖的同步狀態
讀鎖的獲取步驟與寫鎖類似,即不斷的釋放寫鎖狀態,直到爲0時,表示沒有線程獲取讀鎖。

5、自旋鎖 VS 適應性自旋鎖

(1)自旋鎖
讓當前線程進行自旋,如果在自旋完成後前面鎖定同步資源的線程已經釋放了鎖,那麼當前線程就可以不必阻塞而是直接獲取同步資源,從而避免切換線程的開銷。這就是自旋鎖。
自旋鎖的實現原理同樣也是CAS,AtomicInteger中調用unsafe進行自增操作的源碼中的do-while循環就是一個自旋操作,如果修改數值失敗則通過循環來執行自旋,直至修改成功。

缺點:
它不能代替阻塞。自旋等待雖然避免了線程切換的開銷,但它要佔用處理器時間;如果鎖被佔用的時間很短,自旋等待的效果就會非常好。反之,如果鎖被佔用的時間很長,那麼自旋的線程只會白浪費處理器資源。所以,自旋等待的時間必須要有一定的限度,如果自旋超過了限定次數(默認是10次,可以使用-XX:PreBlockSpin來更改)沒有成功獲得鎖,就應當掛起線程。

(2)適應自旋鎖
自適應意味着自旋的時間(次數)不再固定,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,並且持有鎖的線程正在運行中,那麼虛擬機就會認爲這次自旋也是很有可能再次成功,進而它將允許自旋等待持續相對更長的時間。如果對於某個鎖,自旋很少成功獲得過,那在以後嘗試獲取這個鎖時將可能省略掉自旋過程,直接阻塞線程,避免浪費處理器資源。

補充:在自旋鎖中 另有三種常見的鎖形式:TicketLock、CLHlock和MCSlock,其中AQSde CLHlock在把線程封裝成節點後插入CLH後便在做自旋操作。

6、無鎖 、 偏向鎖、量級鎖 和 重量級鎖(難點)

前言:
我們在自旋鎖中提到的“阻塞或喚醒一個Java線程需要操作系統切換CPU狀態來完成,這種狀態轉換需要耗費處理器時間。如果同步代碼塊中的內容過於簡單,狀態轉換消耗的時間有可能比用戶代碼執行的時間還要長”。這種方式就是synchronized最初實現同步的方式,這就是JDK 6之前synchronized效率低的原因。這種依賴於操作系統Mutex Lock所實現的鎖我們稱之爲“重量級鎖”,JDK 6中爲了減少獲得鎖和釋放鎖帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”。

所以目前鎖一共有4種狀態,級別從低到高依次是:無鎖、偏向鎖、輕量級鎖和重量級鎖。鎖狀態只能升級不能降級

四種鎖狀態對應的的Mark Word內容:
在這裏插入圖片描述
(1)無鎖
無鎖沒有對資源進行鎖定,所有的線程都能訪問並修改同一個資源,但同時只有一個線程能修改成功
CAS原理及應用即是無鎖的實現。無鎖無法全面代替有鎖,但無鎖在某些場合下的性能是非常高的。

(2)偏向鎖
偏向鎖是指一段同步代碼一直被一個線程所訪問,那麼該線程會自動獲取鎖,降低獲取鎖的代價
偏向鎖只有遇到其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖,線程不會主動釋放偏向鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有字節碼正在執行),它會首先暫停擁有偏向鎖的線程,判斷鎖對象是否處於被鎖定狀態。撤銷偏向鎖後恢復到無鎖(標誌位爲“01”)或輕量級鎖(標誌位爲“00”)的狀態。

偏向鎖在JDK 6及以後的JVM裏是默認啓用的。可以通過JVM參數關閉偏向鎖:-XX:-UseBiasedLocking=false,關閉之後程序默認會進入輕量級鎖狀態。

(3)輕量級鎖
是指當鎖是偏向鎖的時候,被另外的線程所訪問,偏向鎖就會升級爲輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,從而提高性能

若當前只有一個等待線程,則該線程通過自旋進行等待。但是當自旋超過一定的次數,或者一個線程在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖升級爲重量級鎖。

(4)重量級鎖
升級爲重量級鎖時,鎖標誌的狀態值變爲“10”,此時Mark Word中存儲的是指向重量級鎖的指針,此時等待鎖的線程都會進入阻塞狀態。

在這裏插入圖片描述

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