首先马昨,在 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代碼塊
使用
monitorenter
和monitorexit
指令來實現(xiàn)义桂。
minitorenter
指令編譯后找筝,插入到同步代碼塊開始的位置,monitorexit
指令編譯后慷吊,插入到同步代碼塊結(jié)束的位置和異常處袖裕。JVM
保證每個monitorenter
必須有一個monitorexit
指令與之對應(yīng)。
每個對象都有一個Monitor
對象(監(jiān)視器鎖)與之對應(yīng)溉瓶。
- monitorenter
當線程執(zhí)行到monitorenter
指令的時候急鳄,將會嘗試獲取Monitor
對象的所有權(quán)谤民,過程如下:
- 如果
Monitor
對象的進入計數(shù)器為0
,則該線程成功獲取Monitor
對象的所有權(quán)疾宏,然后將計數(shù)器設(shè)置為1
张足。- 如果該線程已經(jīng)擁有了
Monitor
的所有權(quán),那這次算作是重入坎藐,重入也會將計數(shù)器的值加1
为牍。- 如果其他線程已經(jīng)占有了
Monitor
對象,那么該線程進入阻塞狀態(tài)顺饮,直到Monitor
的計數(shù)器的值為0
吵聪,再重新嘗試獲取Monitor
對象的所有權(quán)。
- monitorexit
當已經(jīng)獲取Monitor
對象所有權(quán)的線程執(zhí)行到monitorexit
指令的時候兼雄,將會釋放Monitor
對象的所有權(quán)吟逝。過程如下:
- 執(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ī)則
下面總結(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
類中的methodA
和methodB
方法都有一個同步代碼塊金句。當線程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
有兩個實例x
和y
罗洗,結(jié)論:
-
x.syncA()
和x.syncB()
不能被同時訪問。因為使用了同一個對象的實例鎖钢猛。 -
x.syncA()
和y.syncB()
可以被同時訪問伙菜。因為使用了不同實例對象的實例鎖。 -
x.cSyncA()
和y.cSyncB()
不能被同時訪問命迈。因為他們使用了同一個全局鎖贩绕,相當于Something
類的鎖火的。 -
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)