淺析 Kotlin 中的 synchronized

首先马昨,在 Java 中 synchronized 是一個關(guān)鍵字,在Kotlin 中是一個函數(shù)稍坯。這個函數(shù)如下:

/*
 * Copyright 2010-2018 JetBrains s.r.o. and Kotlin Programming Language contributors.
 * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
 */

@file:kotlin.jvm.JvmMultifileClass
@file:kotlin.jvm.JvmName("StandardKt")
package kotlin

import kotlin.contracts.*
import kotlin.jvm.internal.unsafe.*

/**
 * Executes the given function [block] while holding the monitor of the given object [lock].
 */
@kotlin.internal.InlineOnly
public actual inline fun <R> synchronized(lock: Any, block: () -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }

    @Suppress("NON_PUBLIC_CALL_FROM_PUBLIC_INLINE", "INVISIBLE_MEMBER")
    monitorEnter(lock)
    try {
        return block()
    }
    finally {
        @Suppress("NON_PUBLIC_CALL_FROM_PUBLIC_INLINE", "INVISIBLE_MEMBER")
        monitorExit(lock)
    }
}

Decompile成字節(jié)碼:

可以看出:這里邊也是有monitorenter和monitorexit的俊鱼,所以做出推測,不管synchronized是java中的關(guān)鍵字還是kotlin中的函數(shù)锹安,最終被編譯成的字節(jié)碼是一樣的短荐。

關(guān)于:contract{ ... } Kotlin 的契約編程, 參考:https://blog.csdn.net/universsky2015/article/details/99011895

Java synchronized 實現(xiàn)原理

在《深入理解Java虛擬機》一書中,介紹了HotSpot虛擬機中叹哭,對象的內(nèi)存布局分為三個區(qū)域:對象頭(Header)忍宋、實例數(shù)據(jù)(Instance Data)和對齊數(shù)據(jù)(Padding)。而對象頭又分為兩個部分“Mark Word”和類型指針风罩,其中“Mark Word”包含了線程持有的鎖糠排。
??因此,synchronized鎖超升,也是保存在對象頭中入宦。JVM基于進入和退出Monitor對象來實現(xiàn)synchronized方法和代碼塊的同步,對于方法和代碼塊的實現(xiàn)細節(jié)又有不同:

代碼塊室琢,使用monitorenter和monitorexit指令來實現(xiàn)乾闰;monitorenter指令編譯后,插入到同步代碼塊開始的位置研乒,monitorexit指令插入到方法同步代碼塊結(jié)束位置和異常處汹忠,JVM保證每個monitorenter必須有一個monitorexit指令與之對應(yīng)。線程執(zhí)行到monitorenter指令處時雹熬,會嘗試獲取對象對應(yīng)的Monitor對象的所有權(quán) (任何一個對象都有一個Monitor對象預(yù)制對應(yīng)宽菜,當一個Monitor被持有后,它將處于鎖定狀態(tài)) 竿报。

方法:在《深入理解Java虛擬機》同步指令一節(jié)中铅乡,關(guān)于方法級的同步描述如下:

方法級的同步是隱式的,即無需通過字節(jié)碼指令來控制烈菌,它實現(xiàn)在方法調(diào)用和返回操作之中阵幸。JVM可以從方法常量池中的方法表結(jié)構(gòu)(method_info Structure) 中的 ACC_SYNCHRONIZED 訪問標志區(qū)分一個方法是否同步方法花履。當方法調(diào)用時,調(diào)用指令將會 檢查方法的 ACC_SYNCHRONIZED 訪問標志是否被設(shè)置挚赊,如果設(shè)置了诡壁,執(zhí)行線程將先持有管程郭宝,然后再執(zhí)行方法娜饵,最后再方法完成(無論是正常完成還是非正常完成)時釋放管程。在方法執(zhí)行期間渣玲,執(zhí)行線程獲取了管程蔑鹦,其他線程就無法獲取管程夺克。

synchronized可以保證方法或者代碼塊在運行時,同一時刻只有一個方法可以進入到臨界區(qū)嚎朽,同時它還可以保證共享變量的內(nèi)存可見性铺纽。

Java中每一個對象都可以作為鎖,這是synchronized實現(xiàn)同步的基礎(chǔ):

  • 普通同步方法哟忍,鎖是當前實例對象
  • 靜態(tài)同步方法狡门,鎖是當前類的class對象
  • 同步方法塊,鎖是括號里面的對象

HotSpot虛擬機中锅很,對象的內(nèi)存布局分為三個區(qū)域:

  • 對象頭(Header
  • 實例數(shù)據(jù)(Instance Data
  • 對齊填充(Padding

其中融撞,對象頭(Header)又分為兩部分:

  • Mark Word
  • 類型指針

synchronized用的鎖是存儲在Java對象頭的Mark Word中的。

下面是Mark Word的存儲結(jié)構(gòu)(32位JVM):

鎖狀態(tài) 25bit 4bit 1bit粗蔚,是否是偏向鎖 2bit,鎖標志位
無鎖狀態(tài) 對象的hashCode 對象分代年齡 0 01

在運行期饶火,Mark Word里存儲的數(shù)據(jù)會隨著標志位的變化而變化鹏控。

存儲內(nèi)容 標志位 狀態(tài)
指向棧中鎖記錄的指針 00 輕量級鎖
指向互斥量(重量級鎖)的指針 10 重量級鎖
空,不需要記錄信息 11 GC標記
偏向線程ID肤寝、偏向時間戳当辐、對象分代年齡 01 偏向鎖

可以看到,Mark Word包含了線程持有的鎖鲤看。

JVM基于進入和退出Monitor對象來實現(xiàn)sunchronized方法和代碼塊的同步缘揪,兩者細節(jié)上有差異。

1.1 synchronized代碼塊

使用monitorentermonitorexit指令來實現(xiàn)义桂。

minitorenter指令編譯后找筝,插入到同步代碼塊開始的位置,monitorexit指令編譯后慷吊,插入到同步代碼塊結(jié)束的位置和異常處袖裕。JVM保證每個monitorenter必須有一個monitorexit指令與之對應(yīng)。

每個對象都有一個Monitor對象(監(jiān)視器鎖)與之對應(yīng)溉瓶。

  • monitorenter

當線程執(zhí)行到monitorenter指令的時候急鳄,將會嘗試獲取Monitor對象的所有權(quán)谤民,過程如下:

  1. 如果Monitor對象的進入計數(shù)器為0,則該線程成功獲取Monitor對象的所有權(quán)疾宏,然后將計數(shù)器設(shè)置為1张足。
  2. 如果該線程已經(jīng)擁有了Monitor的所有權(quán),那這次算作是重入坎藐,重入也會將計數(shù)器的值加1为牍。
  3. 如果其他線程已經(jīng)占有了Monitor對象,那么該線程進入阻塞狀態(tài)顺饮,直到Monitor的計數(shù)器的值為0吵聪,再重新嘗試獲取Monitor對象的所有權(quán)。
  • monitorexit

當已經(jīng)獲取Monitor對象所有權(quán)的線程執(zhí)行到monitorexit指令的時候兼雄,將會釋放Monitor對象的所有權(quán)吟逝。過程如下:

  1. 執(zhí)行monitorexit指令時,Monitor對象的進入計數(shù)器的值減1赦肋,如果減1后的值為0块攒,那么這個線程將會釋放Monitor對象的所有權(quán),其他被這個Monitor阻塞的線程可以開始嘗試去獲取這個Monitor對象的所有權(quán)佃乘。
public class com.fufu.concurrent.SyncCodeBlock {
  public int i;

  public com.fufu.concurrent.SyncCodeBlock();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void syncTask();
    Code:
       0: aload_0
       1: dup
       2: astore_1
       3: monitorenter                      //注意此處囱井,進入同步方法
       4: aload_0
       5: dup
       6: getfield      #2                  // Field i:I
       9: iconst_1
      10: iadd
      11: putfield      #2                  // Field i:I
      14: aload_1
      15: monitorexit                       //注意此處,退出同步方法
      16: goto          24
      19: astore_2
      20: aload_1
      21: monitorexit                      //注意此處趣避,退出同步方法
      22: aload_2
      23: athrow
      24: return
    Exception table:
       from    to  target type
           4    16    19   any
          19    22    19   any
}

1.2 synchronized方法

方法級的同步是隱式的庞呕,即無需通過字節(jié)碼指令來控制,它實現(xiàn)在方法調(diào)用和返回操作之中程帕。

JVM可以從 方法常量池 中的 方法表結(jié)構(gòu)(method_info Structure 中的 ACC_SYNCHRONIZED 訪問標志來辨別一個方法是否聲明為同步方法住练。

當方法調(diào)用時,調(diào)用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標志是否被設(shè)置愁拭,如果設(shè)置了讲逛,執(zhí)行線程將先持有管程,然后再執(zhí)行方法岭埠,最后在方法完成(無論是正常完成還是非正常完成)時釋放管程盏混。在方法執(zhí)行期間,執(zhí)行線程獲取了管程惜论,其他線程就無法獲取管程许赃。

  //省略沒必要的字節(jié)碼
  //==================syncTask方法======================
  public synchronized void syncTask();
    descriptor: ()V
    //方法標識ACC_PUBLIC代表public修飾,ACC_SYNCHRONIZED指明該方法為同步方法
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field i:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field i:I
        10: return
      LineNumberTable:
        line 12: 0
        line 13: 10

2 synchronized使用規(guī)則

參考:https://www.cnblogs.com/skywang12345/p/3479202.html

下面總結(jié)了對象的synchronized基本規(guī)則来涨。

  • 規(guī)則一:當一個線程訪問 “某對象” 的 “synchronized方法” 或者 “synchronized代碼塊” 時图焰,其他線程對“該對象” 的這個 “synchronized方法” 或者這個 “synchronized代碼塊” 的訪問將被阻塞。

  • 規(guī)則二:當一個線程訪問 “某對象” 的 “synchronized方法” 或者 “synchronized代碼塊” 時蹦掐,其他線程對“該對象” 的其他的 “synchronized方法” 或者其他的 “synchronized代碼塊” 的訪問將被阻塞技羔。

  • 規(guī)則三:當一個線程訪問 “某對象” 的 “synchronized方法” 或者 “synchronized代碼塊” 時僵闯,其他線程仍然可以訪問 “該對象” 的非同步代碼塊

2.1 規(guī)則一

當一個線程訪問 “某對象” 的 “synchronized方法” 或者 “synchronized代碼塊” 時藤滥,其他線程對“該對象” 的這個 “synchronized方法” 或者這個 “synchronized代碼塊” 的訪問將被阻塞鳖粟。

public class Demo1 {

    public static void main(String[] args) {

        UserRunnable r = new UserRunnable();
        Thread t1 = new Thread(r, "thread-1");
        Thread t2 = new Thread(r, "thread-2");
        t1.start();
        t2.start();
    }
}

class UserRunnable implements Runnable {

    @Override
    public void run() {
        synchronized (this) {
            try {
                for (int i = 1; i <= 3; i++) {
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + " loop " + i);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

運行結(jié)果:

thread-1 loop 1
thread-1 loop 2
thread-1 loop 3
thread-2 loop 1
thread-2 loop 2
thread-2 loop 3

Process finished with exit code 0

可以看到,線程thread-1獲得了r對象的鎖拙绊,執(zhí)行同步代碼塊向图,線程thread-2只能等待線程thread-1執(zhí)行完了才能開始執(zhí)行。

2.2 規(guī)則二

當一個線程訪問 “某對象” 的 “synchronized方法” 或者 “synchronized代碼塊” 時标沪,其他線程對“該對象” 的其他的 “synchronized方法” 或者其他的 “synchronized代碼塊” 的訪問將被阻塞榄攀。

public class Demo2 {

    public static void main(String[] args) {

        Obj obj = new Obj();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                obj.methadA();
            }
        }, "thread-1");
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                obj.methadB();
            }
        }, "thread-2");
        t1.start();
        t2.start();
    }
}

class Obj {

    public void methadA() {
        synchronized (this) {
            try {
                for (int i = 1; i <= 3; i++) {
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() 
                                       + " call methodA, loop " + i);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void methadB() {
        synchronized (this) {
            try {
                for (int i = 1; i <= 3; i++) {
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() 
                                       + " call methodB, loop " + i);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

運行結(jié)果:

thread-1 call methodA, loop 1
thread-1 call methodA, loop 2
thread-1 call methodA, loop 3
thread-2 call methodB, loop 1
thread-2 call methodB, loop 2
thread-2 call methodB, loop 3

Process finished with exit code 0

可以看到,Obj類中的methodAmethodB方法都有一個同步代碼塊金句。當線程thread-1調(diào)用obj對象的methodA方法的時候檩赢,線程thread-2被阻塞了,直到thread-1釋放了obj對象的鎖违寞,thread-2才開始調(diào)用methodB方法贞瞒。

2.3 規(guī)則三

當一個線程訪問 “某對象” 的 “synchronized方法” 或者 “synchronized代碼塊” 時,其他線程仍然可以訪問 “該對象” 的非同步代碼塊趁曼。

public class Demo3 {

    public static void main(String[] args) {

        Obj obj = new Obj();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                obj.methadA();
            }
        }, "thread-1");
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                obj.methadB();
            }
        }, "thread-2");
        t1.start();
        t2.start();
    }
}

class Obj {

    public void methadA() {
        synchronized (this) {
            try {
                for (int i = 1; i <= 3; i++) {
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() 
                                       + " call methodA, loop " + i);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void methadB() {
        try {
            for (int i = 1; i <= 3; i++) {
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName() 
                                   + " call methodB, loop " + i);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

運行結(jié)果:

thread-1 call methodA, loop 1
thread-2 call methodB, loop 1
thread-1 call methodA, loop 2
thread-2 call methodB, loop 2
thread-1 call methodA, loop 3
thread-2 call methodB, loop 3

Process finished with exit code 0

可以看到军浆,Obj類的methodA方法有同步代碼塊,而methodB方法沒有挡闰。當線程thread-1訪問methodA方法的時候乒融,線程thread-2可以訪問methodB方法,不會阻塞摄悯。

3 實例鎖 和 全局鎖

實例鎖

  • 鎖在某一個實例對象上簇抵。如果該類是單例,那么該鎖也具有全局鎖的概念射众。
  • 實例鎖對應(yīng)的就是 synchronized關(guān)鍵字。

全局鎖

  • 該鎖針對的是類晃财,無論實例多少個對象叨橱,線程都共享該鎖。
  • 全局鎖對應(yīng)的就是 static synchronized關(guān)鍵字(或者是鎖在該類的class或者lassloader對象上)断盛。

例子:

pulbic class Something {
    public synchronized void syncA(){}
    public synchronized void syncB(){}
    public static synchronized void cSyncA(){}
    public static synchronized void cSyncB(){}
}

假設(shè)Something有兩個實例xy罗洗,結(jié)論:

  1. x.syncA()x.syncB()不能被同時訪問。因為使用了同一個對象的實例鎖钢猛。
  2. x.syncA()y.syncB()可以被同時訪問伙菜。因為使用了不同實例對象的實例鎖。
  3. x.cSyncA()y.cSyncB()不能被同時訪問命迈。因為他們使用了同一個全局鎖贩绕,相當于Something類的鎖火的。
  4. x.syncA()Something.cSyncA()可以被同時訪問。因為一個是實例x的鎖淑倾,一個是類Something的鎖馏鹤,不是同一個鎖,互不干擾娇哆。

參考資料

https://juejin.cn/post/6844903830644064264
https://blog.csdn.net/hbtj_1216/article/details/77773292
《深入理解Java虛擬機》
《Java并發(fā)編程藝術(shù)》
【死磕Java并發(fā)】—–深入分析synchronized的實現(xiàn)原理
JVM源碼分析之synchronized實現(xiàn)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末湃累,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子碍讨,更是在濱河造成了極大的恐慌治力,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件勃黍,死亡現(xiàn)場離奇詭異宵统,居然都是意外死亡,警方通過查閱死者的電腦和手機溉躲,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進店門榜田,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人锻梳,你說我怎么就攤上這事箭券。” “怎么了疑枯?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵辩块,是天一觀的道長。 經(jīng)常有香客問我荆永,道長废亭,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任具钥,我火速辦了婚禮豆村,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘骂删。我一直安慰自己掌动,他們只是感情好,可當我...
    茶點故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布宁玫。 她就那樣靜靜地躺著粗恢,像睡著了一般。 火紅的嫁衣襯著肌膚如雪欧瘪。 梳的紋絲不亂的頭發(fā)上眷射,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天,我揣著相機與錄音,去河邊找鬼妖碉。 笑死涌庭,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的嗅绸。 我是一名探鬼主播脾猛,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼鱼鸠!你這毒婦竟也來了猛拴?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤蚀狰,失蹤者是張志新(化名)和其女友劉穎愉昆,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體麻蹋,經(jīng)...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡跛溉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了扮授。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片芳室。...
    茶點故事閱讀 39,731評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖刹勃,靈堂內(nèi)的尸體忽然破棺而出堪侯,到底是詐尸還是另有隱情,我是刑警寧澤荔仁,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布伍宦,位于F島的核電站,受9級特大地震影響乏梁,放射性物質(zhì)發(fā)生泄漏次洼。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一遇骑、第九天 我趴在偏房一處隱蔽的房頂上張望卖毁。 院中可真熱鬧,春花似錦落萎、人聲如沸势篡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至念祭,卻和暖如春兑宇,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背粱坤。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工隶糕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留瓷产,地道東北人。 一個月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓枚驻,卻偏偏與公主長得像濒旦,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子再登,可洞房花燭夜當晚...
    茶點故事閱讀 44,629評論 2 354

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