Java synchronized 關(guān)鍵字原理學(xué)習(xí)

更多并發(fā)相關(guān)內(nèi)容,查看==>Java 線程&并發(fā)學(xué)習(xí)目錄

在上一篇Java 線程 和 鎖 基礎(chǔ)知識(shí)已經(jīng)介紹了Java中的線程和鎖的一些基本概念竣蹦,現(xiàn)在就來(lái)學(xué)習(xí)和了解下Java的內(nèi)置鎖synchronized。具體包含如下幾個(gè)點(diǎn):

  • 類鎖和對(duì)象鎖的用法以及同異;
  • synchronized的優(yōu)化,通過(guò)對(duì)象的頭部結(jié)構(gòu)了解和學(xué)習(xí)偏向鎖、輕量級(jí)鎖范抓、重量級(jí)鎖纸型;
  • 不同的synchronized指令差異以及其說(shuō)明拇砰。

synchronized是Java原生的悲觀鎖、具有可重入的特性狰腌,可保證共享數(shù)據(jù)的線程安全除破。使用時(shí)需要和具體的對(duì)象或類關(guān)聯(lián)綁定。JDK1.5開(kāi)始琼腔,為了提高效率瑰枫,在不同的競(jìng)爭(zhēng)沖突情境下,synchronized也會(huì)出現(xiàn)從無(wú)鎖->偏向鎖->輕量級(jí)鎖->重量級(jí)鎖的單向鎖轉(zhuǎn)變丹莲。

1光坝、synchronized 使用

synchronized可以在對(duì)象尸诽、類以及代碼塊等地方使用,只要不出現(xiàn)活躍性以及發(fā)布不安全等問(wèn)題盯另,一般情況下可以確保單JVM上的共享數(shù)據(jù)安全性含。

對(duì)象使用

public class SynchronizedDemo {
    
    private Object OBJECT = new Object();
    // 鎖標(biāo)識(shí),誰(shuí)占有該對(duì)象就表示占據(jù)該鎖了

    public void testFunction() {
        System.out.println(Thread.currentThread().getName() + " testFunction");
    }

    public synchronized void testSynchronizedFunction() {
        // 對(duì)象方法鎖
        System.out.println(Thread.currentThread().getName() + " testSynchronizedFunction");
        try {
            Thread.sleep(1000 * 10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void testSynchronizedObject() {
        // 對(duì)象代碼塊鎖
        synchronized (this) {
            System.out.println(Thread.currentThread().getName() + " testSynchronizedObject");
            try {
                Thread.sleep(1000 * 10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void testSynchronizedDifferentObject() {
        // 對(duì)象代碼塊鎖土铺,關(guān)聯(lián)的是OBJECT這個(gè)對(duì)象
        synchronized (OBJECT) {
            System.out.println(Thread.currentThread().getName() + " testSynchronizedDifferentObject");
            try {
                Thread.sleep(1000 * 10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void testSynchronizedObjectAgain() {
        // 對(duì)象代碼塊鎖胶滋,重入操作
        synchronized (this) {
            System.out.println(Thread.currentThread().getName() + " testSynchronizedObjectAgain");
            testSynchronizedFunction();
        }
    }
}

再看看下面的測(cè)試demo的效果如何

public class SynchronizedTest {

    public static void testObject() {

        // 同一個(gè)demo,使用對(duì)象鎖的時(shí)候悲敷,只有不是執(zhí)行同一個(gè)

        SynchronizedDemo demo = new SynchronizedDemo();

        Runnable runnable1 = () -> demo.testSynchronizedFunction();
        Runnable runnable2 = () -> demo.testSynchronizedObject();
        Runnable runnable3 = () -> demo.testSynchronizedFunction();

        new Thread(runnable1, "run1").start();
        new Thread(runnable2, "run2").start();
        // new Thread(runnable3, "run3").start();
    }

    public static void testObject1() {

        // 不同的demo究恤,使用對(duì)象鎖的時(shí)候,各自無(wú)影響
        // 因?yàn)殒i住的是對(duì)象后德,不同的對(duì)象之間是隔離開(kāi)的

        SynchronizedDemo demo = new SynchronizedDemo();
        SynchronizedDemo demo1 = new SynchronizedDemo();

        Runnable runnable = () -> demo.testSynchronizedFunction();
        Runnable runnable1 = () -> demo1.testSynchronizedFunction();

        new Thread(runnable, "run").start();
        new Thread(runnable1, "run1").start();
    }

    public static void testObjectAgain() {

        // 同一個(gè)demo部宿,使用對(duì)象鎖后,可以再重入

        SynchronizedDemo demo = new SynchronizedDemo();

        Runnable runnable1 = () -> demo.testSynchronizedObjectAgain();
        Runnable runnable2 = () -> demo.testSynchronizedFunction();

        new Thread(runnable2, "run2").start();
        new Thread(runnable1, "run1").start();
    }
    
    public static void main(String[] args) {
        SynchronizedTest.testObject();
        //SynchronizedTest.testObject1();
        //SynchronizedTest.testObjectAgain();
    }
}

如上main方法中的不同方法調(diào)用瓢湃,輸出的內(nèi)容基本差不多理张,主要是觀察其睡眠暫停的時(shí)間

  • public synchronized:寫在普通的方法上的就表示為「普通同步方法」,他是和當(dāng)前對(duì)應(yīng)的對(duì)象綁定在一起的绵患,不同的線程在調(diào)用同一個(gè)對(duì)象的該方法時(shí)會(huì)發(fā)生競(jìng)爭(zhēng)沖突雾叭,不同對(duì)象則不會(huì)出現(xiàn)競(jìng)爭(zhēng)
  • synchronized (this):寫在代碼塊中的,整體而言和普通方法沒(méi)有本質(zhì)的區(qū)別落蝙,只是和普通方法相比织狐,鎖粒度更細(xì)一些,效率(可能)更高些
  • synchronized (Object):寫在代碼塊中的筏勒,這個(gè)鎖就脫離了當(dāng)前對(duì)象綁定關(guān)系而是和 Object對(duì)象 關(guān)聯(lián)綁定移迫,幾個(gè)不同的類甚至可以通過(guò)傳入同一個(gè)Object實(shí)現(xiàn)不同對(duì)象見(jiàn)的鎖控制,此方法在很多源碼中也被大量使用管行,也建議使用
  • 最后又提及到了可重入厨埋,一個(gè)線程在獲取到鎖后,再獲取該鎖則可以直接獲取捐顷。不過(guò)需要控制好可重入的順序荡陷,如果順序沒(méi)有控制好,再加上資源分配不恰當(dāng)迅涮,會(huì)引發(fā)死鎖的危險(xiǎn)(notify方法也會(huì)引發(fā)死鎖)亲善。

類使用

public class SynchronizedDemo {

    public synchronized static void testStaticFunction() {
        // 類靜態(tài)方法鎖
        System.out.println(Thread.currentThread().getName() + " testStaticFunction");
        try {
            Thread.sleep(1000 * 10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void testClass() {
        synchronized (SynchronizedDemo.class) {
            System.out.println(Thread.currentThread().getName() + " testClass");
            try {
                Thread.sleep(1000 * 10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

demo的測(cè)試用例

public class SynchronizedTest {

    public static void testClass() {

        // 同一個(gè)類,使用類鎖

        SynchronizedDemo demo = new SynchronizedDemo();

        Runnable runnable1 = () -> SynchronizedDemo.testStaticFunction();
        Runnable runnable2 = () -> demo.testClass();

        new Thread(runnable2, "run2").start();
        new Thread(runnable1, "run1").start();
    }

    public static void testClass2() {
        // 一個(gè)類鎖 一個(gè)對(duì)象鎖逗柴,兩者不會(huì)起沖突

        SynchronizedDemo demo = new SynchronizedDemo();

        Runnable runnable1 = () -> demo.testClass();
        Runnable runnable2 = () -> demo.testSynchronizedObject();

        new Thread(runnable1, "run1").start();
        new Thread(runnable2, "run2").start();
    }
}
  • public synchronized static:靜態(tài)方法,和當(dāng)前的類綁定關(guān)聯(lián)顿肺,同一個(gè)類在調(diào)用類似方法時(shí)戏溺,會(huì)出現(xiàn)競(jìng)爭(zhēng)沖突
  • synchronized (XXX.class)綁定的是指定的類XXX.class渣蜗,存在幾個(gè)不同的對(duì)象,方法中使用同一個(gè)類的情況
  • 同一個(gè)對(duì)象的類鎖和對(duì)象鎖之間不會(huì)出現(xiàn)競(jìng)爭(zhēng)沖突

2旷祸、synchronized 優(yōu)化

JVM結(jié)構(gòu)分為程序計(jì)數(shù)器耕拷、虛擬機(jī)棧本地方法棧托享、方法區(qū)以及骚烧,而創(chuàng)建的對(duì)象信息則是存放在堆中

JVM結(jié)構(gòu)

虛擬機(jī)棧:對(duì)象的方法調(diào)用臨時(shí)申請(qǐng)的數(shù)據(jù)存放點(diǎn)、方法接口等信息闰围,A方法調(diào)用B方法赃绊,再調(diào)用C方法,這些關(guān)系就是存放在虛擬機(jī)棧中的羡榴,日常所說(shuō)的打印出錯(cuò)誤的堆棧信息也就存在棧中
本地方法棧:方法調(diào)用的本地native方法
方法區(qū):線程共享的區(qū)域(永生代)碧查,存儲(chǔ)類加載器加載的類信息、常量校仑、靜態(tài)變量等信息忠售,例如static和final
堆:對(duì)象實(shí)例存放點(diǎn)(包含新生代和老年代),新建的對(duì)象信息都是存放在堆中的
程序計(jì)數(shù)器:可以認(rèn)為是下一條需要執(zhí)行的指令指示器

對(duì)象堆的組成區(qū)域如下圖迄沫,其中數(shù)據(jù)實(shí)例是類的具體內(nèi)容稻扬,而對(duì)齊填充則是JVM的約定,所有對(duì)象的大小必須是8字節(jié)的倍數(shù)羊瘩,例如某個(gè)對(duì)象包含對(duì)象頭是63個(gè)字節(jié)泰佳,那么對(duì)齊填充則是1個(gè)字節(jié)。而和synchronized最密切的是對(duì)象頭中的MarkWord 標(biāo)記字段困后。

image

在標(biāo)記字段值也包含了很多內(nèi)容乐纸,例如HashCode,鎖標(biāo)志位等等摇予。具體如下圖在不同的鎖情況下汽绢,64位的MarkWord內(nèi)容。隨著競(jìng)爭(zhēng)的加大侧戴,synchronized會(huì)從無(wú)鎖->偏向鎖->輕量級(jí)鎖->重量級(jí)鎖轉(zhuǎn)變的

image

該圖來(lái)源自:https://blog.csdn.net/scdn_cp/article/details/86491792

  • 無(wú)鎖:鎖對(duì)象剛剛創(chuàng)建宁昭,沒(méi)有競(jìng)爭(zhēng),偏向鎖標(biāo)識(shí)位為0酗宋,鎖狀態(tài)是01
  • 偏向鎖:出現(xiàn)一個(gè)線程競(jìng)爭(zhēng)积仗,則直接把當(dāng)前的線程信息記錄到當(dāng)前對(duì)象中,并且只偏愛(ài)蜕猫,同時(shí)偏向鎖標(biāo)識(shí)位是為1
  • 輕量級(jí)鎖:出現(xiàn)大于等于2個(gè)線程競(jìng)爭(zhēng)時(shí)寂曹,就不再偏愛(ài)了,鎖從偏向鎖升級(jí)為輕量級(jí)鎖,并記錄下競(jìng)爭(zhēng)成功的線程記錄隆圆,鎖狀態(tài)是00
  • 重量級(jí)鎖:競(jìng)爭(zhēng)更加嚴(yán)重漱挚,鎖升級(jí)為重量級(jí)鎖(也叫同步鎖),現(xiàn)在MarkWord中指向的不再是線程信息渺氧,而是Monitor監(jiān)視器信息旨涝,同時(shí)鎖狀態(tài)是10
  • 被GC標(biāo)記的對(duì)象:待回收了,只要下一次GC不再被引用就會(huì)被回收掉的侣背,鎖狀態(tài)是11
  • 監(jiān)視器Monitor:和每一個(gè)對(duì)象都有一根無(wú)形的線關(guān)聯(lián)著白华,監(jiān)視器記錄著關(guān)聯(lián)的對(duì)象、持有的線程贩耐、阻塞的線程信息等

3弧腥、synchronized 底層實(shí)現(xiàn)

java 文件通過(guò)編譯后生成了class文件,再使用javap -verbose XXXX文件輸出字節(jié)碼憔杨,為了便于說(shuō)明問(wèn)題新加非常小的demo文件測(cè)試一下

public class SimpleClass {

    private Object obj = new Object();

    public synchronized void run() {
        // 同步方法
    }

    public void run1() {
        // 同步代碼塊
        synchronized (this) {}
    }

    public void run2() {
        // 同步指定的對(duì)象
        synchronized (obj) {}
    }

    public void run3() {
        // 同步指定的類
        synchronized (SimpleClass.class) {}
    }
}

其中run() 和 run1() 從功能上來(lái)說(shuō)是完全一致的鸟赫,都是綁定當(dāng)前對(duì)象,查看相關(guān)指令如下代碼(除去了無(wú)關(guān)指令)

  public synchronized void run();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=1, args_size=1
         0: return
        ....

  public void run1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: aload_1
         5: monitorexit
         6: goto          14
         9: astore_2
        10: aload_1
        11: monitorexit
        12: aload_2
        13: athrow
        14: return
        .....

雖然這兩者的功能完全一致消别,但是具體的底層實(shí)現(xiàn)卻不一樣抛蚤,同步方法是直接添加了flagACC_SYNCHRONIZED標(biāo)識(shí)其是一個(gè)同步的方法,而同步代碼塊則是使用了1條monitorenter指令和2條monitorexit指令寻狂,其中有2條monitorexit的原因主要是編譯器自動(dòng)產(chǎn)生一個(gè)異常處理器岁经,后面一個(gè)monitorexit就是在異常處理結(jié)束后釋放monitor的

  public void run2();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: getfield      #3                  // Field obj:Ljava/lang/Object;
         4: dup
         5: astore_1
         6: monitorenter
         7: aload_1
         8: monitorexit
         9: goto          17
        12: astore_2
        13: aload_1
        14: monitorexit
        15: aload_2
        16: athrow
        17: return
        ...
        
  public void run3();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #4                  // class new2019/Synchronized/SimpleClass
         2: dup
         3: astore_1
         4: monitorenter
         5: aload_1
         6: monitorexit
         7: goto          15
        10: astore_2
        11: aload_1
        12: monitorexit
        13: aload_2
        14: athrow
        15: return
        ....

而和run1()相比,run2()中的指令就僅僅只多了一句指令1: getfield #3蛇券,獲取被管理的對(duì)象object缀壤,用來(lái)替換默認(rèn)的this,run3()的指令更加簡(jiǎn)單直接就是0: ldc #4,把#4(SimpleClass.class)推送到了當(dāng)前的棧頂

這樣看來(lái)使用synchronized(XX)的方法從底層指令而言沒(méi)有太大的差異纠亚,就是加載了不同的數(shù)據(jù)進(jìn)行處理塘慕,有的是當(dāng)前對(duì)象,有的是指定對(duì)象蒂胞,有的是指定的類信息图呢,但是因?yàn)榧虞d的數(shù)據(jù)不同,使得持有的鎖也是完全不一樣的骗随,類對(duì)象會(huì)持有關(guān)聯(lián)一個(gè)監(jiān)視器蛤织,類Class也會(huì)持有一個(gè)監(jiān)視器

關(guān)于Monitor和MarkWord的C++底層實(shí)現(xiàn)原理可以看看HostSpot源碼

4、參考鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末鸿染,一起剝皮案震驚了整個(gè)濱河市指蚜,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌涨椒,老刑警劉巖摊鸡,帶你破解...
    沈念sama閱讀 221,888評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件绽媒,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡柱宦,警方通過(guò)查閱死者的電腦和手機(jī)些椒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)掸刊,“玉大人,你說(shuō)我怎么就攤上這事赢乓∮遣啵” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 168,386評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵牌芋,是天一觀的道長(zhǎng)蚓炬。 經(jīng)常有香客問(wèn)我,道長(zhǎng)躺屁,這世上最難降的妖魔是什么肯夏? 我笑而不...
    開(kāi)封第一講書人閱讀 59,726評(píng)論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮犀暑,結(jié)果婚禮上驯击,老公的妹妹穿的比我還像新娘。我一直安慰自己耐亏,他們只是感情好徊都,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,729評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著广辰,像睡著了一般暇矫。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上择吊,一...
    開(kāi)封第一講書人閱讀 52,337評(píng)論 1 310
  • 那天李根,我揣著相機(jī)與錄音,去河邊找鬼几睛。 笑死房轿,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的枉长。 我是一名探鬼主播冀续,決...
    沈念sama閱讀 40,902評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼必峰!你這毒婦竟也來(lái)了洪唐?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 39,807評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤吼蚁,失蹤者是張志新(化名)和其女友劉穎凭需,沒(méi)想到半個(gè)月后问欠,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,349評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡粒蜈,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,439評(píng)論 3 340
  • 正文 我和宋清朗相戀三年顺献,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片枯怖。...
    茶點(diǎn)故事閱讀 40,567評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡注整,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出度硝,到底是詐尸還是另有隱情肿轨,我是刑警寧澤,帶...
    沈念sama閱讀 36,242評(píng)論 5 350
  • 正文 年R本政府宣布蕊程,位于F島的核電站椒袍,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏藻茂。R本人自食惡果不足惜驹暑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,933評(píng)論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望辨赐。 院中可真熱鬧优俘,春花似錦、人聲如沸肖油。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,420評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)森枪。三九已至视搏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間县袱,已是汗流浹背浑娜。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,531評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留式散,地道東北人筋遭。 一個(gè)月前我還...
    沈念sama閱讀 48,995評(píng)論 3 377
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像暴拄,于是被迫代替她去往敵國(guó)和親漓滔。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,585評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容