【并發(fā)編程系列2】synchronized鎖升級(jí)原理分析(偏向鎖-輕量級(jí)鎖-重量級(jí)鎖)

初識(shí) synchronized

在并發(fā)編程中,synchronized對我們來說并不陌生,我們都知道,當(dāng)多個(gè)線程并行的情況下,程序是不安全的,這個(gè)不安全主要發(fā)生在共享變量的不安全,我們通過一個(gè)例子來說明:

package com.zwx.concurrent;

public class TestSynchronized {
    private static int count;

    public static void increment(){
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count++;

    }

    public static void main(String[] args) throws InterruptedException {
        for (int i=0;i<1000;i++){
            new Thread(()->TestSynchronized.increment()).start();
        }
        Thread.sleep(3000);
        System.out.println("結(jié)果:" + count);
    }
}

這里的輸出結(jié)果我們預(yù)期是1000,然而實(shí)際上并不一定會(huì)輸出1000,產(chǎn)生這種狀況的原因是存在如下場景:
1、線程1獲取count為0,這時(shí)候他去執(zhí)行count++(非原子操作)
2、線程2又去獲取count,這時(shí)候因?yàn)榫€程A還沒有返回結(jié)果,所以依然獲取到0
3、線程1執(zhí)行count++后得到count=1,寫回內(nèi)存
4、線程2執(zhí)行count++后得到count=1,寫回內(nèi)存
5、線程3去獲取count,這時(shí)候獲取到count為1,然而實(shí)際上已經(jīng)執(zhí)行過2次count++操作了
假如線程是按照上面的1-5個(gè)步驟執(zhí)行的話,就會(huì)導(dǎo)致最后的結(jié)果不會(huì)輸出1000,那么如何解決這個(gè)問題呢?就是在increment()方法上加上synchronized關(guān)鍵字

synchronized 用法

synchronized 有三種方式來加鎖,分別是:

  • 修飾實(shí)例方法,作用于當(dāng)前實(shí)例加鎖,進(jìn)入同步代碼前要獲得當(dāng)前實(shí)例的鎖
public synchronized void test(){
        System.out.println("修飾實(shí)例方法");
    }

  • 修飾靜態(tài)方法,作用于當(dāng)前類對象加鎖,進(jìn)入同步代碼前要獲得當(dāng)前類對象的鎖
public static synchronized void test2(){
        System.out.println("修飾靜態(tài)方法");
    }

  • 修飾代碼塊,指定加鎖對象,對給定對象加鎖,進(jìn)入同步代碼庫前要獲得給定對象的鎖
public void test3(){
        synchronized (this){
            System.out.println("修飾代碼塊");
        }
    }

鎖是如何存儲(chǔ)的

我們每個(gè)人在學(xué)習(xí)java中接觸到的最多的一句話之一我想肯定是:一切皆對象。鎖就是一個(gè)對象,那么這個(gè)對象里面的結(jié)構(gòu)是怎么樣的呢,鎖對象里面都保存了哪些信息呢?
在Hotspot 虛擬機(jī)中,對象在內(nèi)存中的存儲(chǔ)布局,可以分為三個(gè)區(qū)域:對象頭(Header)、實(shí)例數(shù)據(jù)(Instance Data)、對齊填充(Padding)。synchronized用的鎖是存在Java對象頭里的,Java對象頭里面包含兩部分信息:
第一部分官方稱之為“Mark Word” ,用于存儲(chǔ)自身的運(yùn)行時(shí)數(shù)據(jù),如:HashCode,GC分代年齡,鎖標(biāo)記、偏向鎖線程ID等;第二部分是類型指針,即對象指向它的類元信息,虛擬機(jī)通過這個(gè)指針來確定這個(gè)對象是哪個(gè)類的實(shí)例(如果java對象是一個(gè)數(shù)組,那么對象頭中還必須有一塊用于記錄數(shù)組長度的數(shù)據(jù))
到這里我們就知道了,鎖是記錄在對象頭中的“Mark Word”,那么“Mark Word”又是如何存儲(chǔ)鎖的信息的呢?
在32位虛擬機(jī)中,“Mark Word”存儲(chǔ)結(jié)構(gòu)如下圖:

在這里插入圖片描述

在64位虛擬機(jī)中,“Mark Word”存儲(chǔ)結(jié)構(gòu)如下圖:

在這里插入圖片描述

synchronized 鎖升級(jí)

在多線程并發(fā)編程中synchronized 一直是元老級(jí)角色,很多人都會(huì)稱呼它為重量級(jí)鎖。但是隨著Java SE 1.6 對synchronized 進(jìn)行了各種優(yōu)化之后,有些情況下它就并不那么重,Java SE 1.6 中為了減少獲得鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級(jí)鎖。
在Java SE 1.6中,鎖一共有4種狀態(tài),級(jí)別從低到高依次是:無鎖狀態(tài)、偏向鎖狀態(tài)、輕量級(jí)鎖狀態(tài)重量級(jí)鎖狀態(tài),這幾個(gè)狀態(tài)會(huì)隨著競爭情況逐漸升級(jí)。至于鎖的降級(jí)并沒有一個(gè)標(biāo)準(zhǔn),在達(dá)到一定的苛刻條件之后可以進(jìn)行降級(jí),但是一般情況我們可以簡單的認(rèn)為鎖不可以降級(jí),這里不做過多的敘述。

偏向鎖

HotSpot的作者經(jīng)過研究發(fā)現(xiàn),大多數(shù)情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,所以為了讓線程獲得鎖的代價(jià)更低而引入了偏向鎖。
當(dāng)一個(gè)線程訪問加了同步鎖的代碼塊時(shí),會(huì)在對象頭中存儲(chǔ)當(dāng)前線程的 ID,后續(xù)這個(gè)線程進(jìn)入和退出這段加了同步鎖的代碼塊時(shí),不需要再次加鎖和釋放鎖。而是直接比較對象頭里面是否存儲(chǔ)了指向當(dāng)前線程的線程ID。如果相等表示偏向鎖是偏向于當(dāng)前線程的,就不需要再嘗試獲得鎖了。

偏向鎖的獲取

1、首先獲取鎖對象頭中的 Mark Word,判斷當(dāng)前對象是否處于可偏向狀態(tài)(即當(dāng)前沒有對象獲得偏向鎖)。
2、如果是可偏向狀態(tài),則通過CAS原子操作,把當(dāng)前線程的ID寫入到 MarkWord,如果CAS成功,表示獲得偏向鎖成功,會(huì)將偏向鎖標(biāo)記設(shè)置為1,且將當(dāng)前線程的ID寫入Mark Word;如果CAS失敗則說明當(dāng)前有其他線程獲得了偏向鎖,同時(shí)也說明當(dāng)前環(huán)境存在鎖競爭,這時(shí)候就需要將已獲得偏向鎖的線程中的偏向鎖撤銷掉(具體參考下面偏向鎖的撤銷),并升級(jí)為輕量級(jí)鎖。
3、如果當(dāng)前線程是已偏向狀態(tài),需要檢查Mark Word中的ThreadID是否和自己相等,如果相等則不需要再次獲得鎖,可以直接執(zhí)行同步代碼塊,如果不相等,說明當(dāng)前偏向的是其他線程,需要撤銷偏向鎖并升級(jí)到輕量級(jí)鎖。

偏向鎖的撤銷

偏向鎖的撤銷,需要等待全局安全點(diǎn)(即在這個(gè)時(shí)間點(diǎn)上沒有正在執(zhí)行的字節(jié)碼),然后會(huì)暫停擁有偏向鎖的線程,并檢查持有偏向鎖的線程是否活著,主要有以下兩種情況:

  1. 如果線程不處于活動(dòng)狀態(tài),則將對象頭設(shè)置成無鎖狀態(tài)。
  2. 如果線程仍然活著,擁有偏向鎖的棧會(huì)被執(zhí)行,遍歷偏向?qū)ο蟮逆i記錄,棧中的鎖記錄和對象頭的Mark Word要么重新偏向于其他線程(重偏向需要滿足批量重偏向的條件),要么恢復(fù)到無鎖或者標(biāo)記對象不適合作為偏向鎖。

最后喚醒暫停的線程。

偏向鎖的批量重偏向

一個(gè)線程創(chuàng)建了大量對象而且執(zhí)行了同步操作后另一個(gè)線程又來將這些對象作為鎖對象進(jìn)行操作,并且達(dá)到閾值,此時(shí)就會(huì)發(fā)生偏向鎖重偏向的操作(除了這種情況,其他情況只有有線程來競爭鎖,則偏向鎖狀態(tài)就結(jié)束了)。
-XX:BiasedLockingBulkRebiasThreshold 為重偏向閾值JVM參數(shù),默認(rèn)20,可以通過-XX:+PrintFlagsFinal打印出默認(rèn)參數(shù),接下來我們通過一個(gè)示例來演示一下批量重偏向:

<dependency>
     <groupId>org.openjdk.jol</groupId>
     <artifactId>jol-core</artifactId>
     <version>0.10</version>
 </dependency>

package com.zwx.concurrent;

import com.zwx.model.User;
import org.openjdk.jol.info.ClassLayout;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class BiasedLockDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000);//默認(rèn)延遲4s才會(huì)開啟偏向鎖,休眠5s確保開啟偏向鎖

        List<User> list = new ArrayList<>();
        new Thread(()->{
            for (int i=0;i<20;i++){
                //這里必須要new不同的對象,不能共用同一個(gè)對象
                User user = new User();//只是一個(gè)空對象
                synchronized (user){
                    list.add(user);
                    System.out.println("t1線程第" + (i+1) + "對象:" + ClassLayout.parseInstance(user).toPrintable());
                }
            }
        },"t1").start();

        try {
            Thread.sleep(10000);//確保t1創(chuàng)建對象完畢
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("------------------------------------------------------");
        new Thread(()->{
            for (int j=0;j<20;j++){
                User user = list.get(j);
                synchronized (user){
                    System.out.println("t2線程第" + (j+1) + "對象:" + ClassLayout.parseInstance(user).toPrintable());
                }
            }
        },"t2").start();
    }
}

運(yùn)行結(jié)果部分截圖(t1線程肯定是101,就不截圖了,t2前面19個(gè)都是000,第20個(gè)達(dá)到閾值了,發(fā)生了重偏向):
101三位數(shù)說明:
第一位:0-表示非偏向 1-表示偏向
后兩位:00-表示輕量級(jí)鎖 01-表示偏向鎖 10表示重量級(jí)鎖


在這里插入圖片描述

當(dāng)然,有批量重偏向,也有批量撤銷,在這里就不做過多敘述,以后有時(shí)間了可以單獨(dú)更深入的寫一寫,感興趣的可以關(guān)注留意!

偏向鎖及撤銷流程圖

在這里插入圖片描述

偏向鎖注意事項(xiàng)

偏向鎖在Java SE 1.6和Java SE 1.7里是默認(rèn)啟用的,但是它在應(yīng)用程序啟動(dòng)幾秒鐘之后才激活,如有必要可以使用JVM參數(shù)來關(guān)閉延遲:-XX:BiasedLockingStartupDelay=0。如果你確定應(yīng)用程序里所有的鎖通常情況下都處于競爭狀態(tài),可以通過JVM參數(shù)關(guān)閉偏向鎖:-XX:- UseBiasedLocking=false,那么程序默認(rèn)會(huì)進(jìn)入輕量級(jí)鎖狀態(tài)。
如果我們的應(yīng)用中大多數(shù)情況存在線程競爭,那么建議是關(guān)閉偏向鎖,因?yàn)殚_啟反而會(huì)因?yàn)槠蜴i撤銷操作而引起更多的資源消耗。

輕量級(jí)鎖

輕量級(jí)鎖,一般用于兩個(gè)線程在交替使用鎖的時(shí)候,由于沒有同時(shí)搶鎖,屬于一種比較和諧的狀態(tài),就可以使用輕量級(jí)鎖。

輕量級(jí)鎖加鎖

線程在執(zhí)行同步代碼塊之前,JVM會(huì)先在當(dāng)前線程的棧楨中創(chuàng)建用于存儲(chǔ)鎖記錄的空間,并將對象頭中的Mark Word復(fù)制到鎖記錄中,官方稱為Displaced Mark Word。然后線程嘗試使用 CAS將對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,當(dāng)前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當(dāng)前線程便嘗試使用自旋來獲取鎖。

輕量級(jí)鎖解鎖

輕量級(jí)解鎖時(shí),會(huì)使用原子的CAS操作將Displaced Mark Word替換回到對象頭,如果成功,則表示沒有競爭發(fā)生。如果失敗,表示當(dāng)前鎖存在競爭,鎖就會(huì)膨脹成重量級(jí)鎖

輕量級(jí)鎖及膨脹流程圖

在這里插入圖片描述

自旋鎖

輕量級(jí)鎖在加鎖過程中,用到了自旋鎖。所謂自旋,就是指當(dāng)有另外一個(gè)線程來競爭鎖時(shí),這個(gè)線程會(huì)在原地循環(huán)等待,而不是把該線程給阻塞,直到那個(gè)獲得鎖的線程釋放鎖之后,這個(gè)線程就可以馬上獲得鎖的。
為什么要采用自旋等待呢?
因?yàn)榻^大多數(shù)情況下線程獲得鎖和釋放鎖的過程都是非常短暫的,自旋一定次數(shù)之后極有可能碰到獲得鎖的線程釋放鎖,所以,輕量級(jí)鎖適用于那些同步代碼塊執(zhí)行很快的場景,這樣,線程原地等待很短的時(shí)間就能夠獲得鎖了。
注意:鎖在原地循環(huán)等待的時(shí)候,是會(huì)消耗CPU資源的。所以自旋必須要有一定的條件控制,否則如果一個(gè)線程執(zhí)行同步代碼塊的時(shí)間很長,那么等待鎖的線程會(huì)不斷的循環(huán)反而會(huì)消耗CPU資源。默認(rèn)情況下鎖自旋的次數(shù)是 10 次,可以使用-XX:PreBlockSpin參數(shù)來設(shè)置自旋鎖等待的次數(shù)。

自適應(yīng)自旋

在 JDK1.7 開始,引入了自適應(yīng)自旋鎖,修改自旋鎖次數(shù)的JVM參數(shù)被取消,虛擬機(jī)不再支持由用戶配置自旋鎖次數(shù),而是由虛擬機(jī)自動(dòng)調(diào)整。自適應(yīng)意味著自旋的次數(shù)不是固定不變的,而是根據(jù)前一次在同一個(gè)鎖上自旋的時(shí)間以及鎖的擁有者的狀態(tài)來決定。如果在同一個(gè)鎖對象上,自旋等待剛剛成功獲得過鎖,并且持有鎖的線程正在運(yùn)行中,那么虛擬機(jī)就會(huì)認(rèn)為這次自旋也是很有可能再次成功,進(jìn)而它將允許自旋等待持續(xù)相對更長的時(shí)間。如果對于某個(gè)鎖,自旋很少成功獲得過,那在以后嘗試獲取這個(gè)鎖時(shí)將可能省略掉自旋過程,直接阻塞線程,避免浪費(fèi)處理器資源。

重量級(jí)鎖

當(dāng)輕量級(jí)鎖膨脹到重量級(jí)鎖之后,意味著線程只能被掛起阻塞來等待喚醒了。每一個(gè)對象中都有一個(gè)Monitor監(jiān)視器,而Monitor依賴操作系統(tǒng)的 MutexLock(互斥鎖)來實(shí)現(xiàn)的, 線程被阻塞后便進(jìn)入內(nèi)核(Linux)調(diào)度狀態(tài),這個(gè)會(huì)導(dǎo)致系統(tǒng)在用戶態(tài)與內(nèi)核態(tài)之間來回切換,嚴(yán)重影響鎖的性能。
monitorenter指令是在編譯后插入到同步代碼塊的開始位置,而monitorexit是插入到方法結(jié)束處和異常處,JVM要保證每個(gè)monitorenter必須有對應(yīng)的monitorexit與之配對。而且當(dāng)一個(gè)monitor被持有后,它將處于鎖定狀態(tài)。線程執(zhí)行到monitorenter指令時(shí),將會(huì)嘗試獲取對象所對應(yīng)的monitor的所有權(quán),即嘗試獲得對象的鎖。我們可以簡單的理解為,在加重量級(jí)鎖的時(shí)候會(huì)執(zhí)行monitorenter指令,解鎖時(shí)會(huì)執(zhí)行monitorexit指令。

鎖的優(yōu)缺點(diǎn)對比

優(yōu)點(diǎn) 缺點(diǎn) 適用場景
偏向鎖 加鎖和解鎖不需要額外的消耗,和執(zhí)行非同步代碼塊僅存在納秒級(jí)差距 如果線程間存在鎖競爭,會(huì)帶來額外的鎖撤銷消耗 適用于只有一個(gè)線程訪問同步代碼塊場景
輕量級(jí)鎖 競爭的線程不會(huì)阻塞,提高了程序的響應(yīng)速度 如果始終得不到鎖,使用自旋會(huì)消耗CPU 追求響應(yīng)時(shí)間;同步代碼塊執(zhí)行時(shí)間非常短
重量級(jí)鎖 線程競爭不使用自旋,不會(huì)消耗CPU 線程阻塞,響應(yīng)時(shí)間緩慢 追求吞吐量;同步代碼塊執(zhí)行時(shí)間較長

總結(jié)

synchronized可以解決并發(fā)編程中的三大問題:原子性可見性有序性,雖然JDK對其做了優(yōu)化,有些時(shí)候并不那么重了,但是在某些場景中,我們可以使用[volatile關(guān)鍵字]代替synchronized,如果volatile變量修飾符使用恰當(dāng)?shù)脑?,它比synchronized的使用和執(zhí)行成本更低,因?yàn)樗粫?huì)引起線程上下文的切換和調(diào)度。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容