深入理解synchronized關鍵字
synchronized是併發程式設計中重要的使用工具之一,我們必須學會使用並且掌握它的原理。
概念及作用
JVM自帶的關鍵字,可在需要執行緒安全的業務場景中使用,來保證執行緒安全。
用法
按照鎖的物件區分可以分為物件鎖和類鎖 按照在程式碼中的位置區分可以分為方法形式和程式碼塊形式
物件鎖
鎖物件為當前this或者說是當前類的例項物件
public void synchronized method() {
System.out.println("我是普通方法形式的物件鎖");
}
public void method() {
synchronized(this) {
System.out.println("我是程式碼塊形式的物件鎖");
}
}
複製程式碼
類鎖
鎖的是當前類或者指定類的Class物件。一個類可能有多個例項物件,但它只可能有一個Class物件。
public static void synchronized method() {
System.out.println("我是靜態方法形式的類鎖");
}
public void method() {
synchronized(*.class) {
System.out.println("我是程式碼塊形式的類鎖");
}
}
複製程式碼
SimpleExample
參考 慕課網 《Java高併發之魂:synchronized深度解析》
最基本的用法在上一個標題用法中已將虛擬碼列出,這裡列舉在以上基礎上稍微變化一些的用法 1.多個例項,對當前例項加鎖,同步執行,對當前類Class物件加鎖,非同步執行
public class SimpleExample implements Runnable {
static SimpleExample instance1 = new SimpleExample();
static SimpleExample instance2 = new SimpleExample();
@Override
public void run() {
method1();
method2();
method3();
method4();
}
public synchronized void method1() {
common();
}
public static synchronized void method2() {
commonStatic();
}
public void method3() {
synchronized(this) {
common();
}
}
public void method4() {
synchronized(MultiInstance.class) {
common();
}
}
public void method5() {
common();
}
public void method6() {
commonStatic();
}
public void common() {
System.out.println("執行緒 " + Thread.currentThread().getName() + " 正在執行");
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println("執行緒 " + Thread.currentThread().getName() + " 執行完畢");
}
public static void commonStatic() {
System.out.println("執行緒 " + Thread.currentThread().getName() + " 正在執行");
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println("執行緒 " + Thread.currentThread().getName() + " 執行完畢");
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance1);
Thread t2 = new Thread(instance2);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("finished");
}
}
method1()、method3()結果為:
執行緒 Thread-0 正在執行
執行緒 Thread-1 正在執行
執行緒 Thread-0 執行完畢
執行緒 Thread-1 執行完畢
finished
method2()、method4()執行結果為:
執行緒 Thread-0 正在執行
執行緒 Thread-0 執行完畢
執行緒 Thread-1 正在執行
執行緒 Thread-1 執行完畢
finished
複製程式碼
2.物件鎖和類鎖,鎖的物件不一樣,互不影響,所以非同步執行
// 將run方法改為
@Override
public void run() {
if("Thread-0".equals(Thread.currentThread().getName())) {
method1();
} else {
method2();
}
}
// 將main方法改為
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance1);
Thread t2 = new Thread(instance1);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("finished");
}
結果為:
執行緒 Thread-0 正在執行
執行緒 Thread-1 正在執行
執行緒 Thread-1 執行完畢
執行緒 Thread-0 執行完畢
finished
複製程式碼
3.物件鎖和無鎖得普通方法,普通方法不需要持有鎖,所以非同步執行
// 將run方法改為
@Override
public void run() {
if("Thread-0".equals(Thread.currentThread().getName())) {
method1();
} else {
method5();
}
}
// main方法同 2
結果為:
執行緒 Thread-0 正在執行
執行緒 Thread-1 正在執行
執行緒 Thread-0 執行完畢
執行緒 Thread-1 執行完畢
finished
複製程式碼
4.類鎖和無鎖靜態方法,非同步執行
// 將run方法改為
@Override
public void run() {
if("Thread-0".equals(Thread.currentThread().getName())) {
method1();
} else {
method6();
}
}
// main方法同 2
結果為:
執行緒 Thread-0 正在執行
執行緒 Thread-1 正在執行
執行緒 Thread-0 執行完畢
執行緒 Thread-1 執行完畢
finished
複製程式碼
5.方法丟擲異常,synchronized鎖自動釋放
// run方法改為
@Override
public void run() {
if ("Thread-0".equals(Thread.currentThread().getName())) {
method7();
} else {
method8();
}
}
public synchronized void method7() {
...
throw new RuntimeException();
}
public synchronized void method8() {
common();
}
public static void main(String[] args) throws InterruptedException {
// 同 2
}
結果為:
我是丟擲異常的加鎖方法,我叫 thread---------1
我是沒有異常的加鎖方法,我叫 thread---------2
Exception in thread "thread---------1" java.lang.RuntimeException
at com.marksman.theory2practicehighconcurrency.synchronizedtest.SynchronizedException9.method1(SynchronizedException9.java:29)
at com.marksman.theory2practicehighconcurrency.synchronizedtest.SynchronizedException9.run(SynchronizedException9.java:15)
at java.lang.Thread.run(Thread.java:748)
thread---------2 執行結束
finished
// 這說明丟擲異常後持有物件鎖的method7()方法釋放了鎖,這樣method8()才能獲取到鎖並執行。
複製程式碼
6.可重入特性
public class SynchronizedRecursion {
int a = 0;
int b = 0;
private void method1() {
System.out.println("method1正在執行,a = " + a);
if (a == 0) {
a ++;
method1();
}
System.out.println("method1執行結束,a = " + a);
}
private synchronized void method2() {
System.out.println("method2正在執行,b = " + b);
if (b == 0) {
b ++;
method2();
}
System.out.println("method2執行結束,b = " + b);
}
public static void main(String[] args) {
SynchronizedRecursion synchronizedRecursion = new SynchronizedRecursion();
synchronizedRecursion.method1();
synchronizedRecursion.method2();
}
}
結果為:
method1正在執行,a = 0
method1正在執行,a = 1
method1執行結束,a = 1
method1執行結束,a = 1
method2正在執行,b = 0
method2正在執行,b = 1
method2執行結束,b = 1
method2執行結束,b = 1
// 可以看到method1()與method2()的執行結果一樣的,method2()在獲取到物件鎖以後,在遞迴呼叫時不需要等上一次呼叫先釋放後再獲取,而是直接進入,這說明了synchronized的可重入性.
// 當然,除了遞迴呼叫,呼叫同類的其它同步方法,呼叫父類同步方法,都是可重入的,前提是同一物件去呼叫,這裡就不一一列舉了.
複製程式碼
總結一下
- 一把鎖只能同時被一個執行緒獲取,沒有拿到鎖的執行緒必須等待;
- 每個例項都對應有自己的一把鎖,不同例項之間互不影響;
- 鎖物件是*.class以及synchronized修飾的static方法時,所有物件共用一把類鎖;
- 無論是方法正常執行完畢或者方法丟擲異常,都會釋放鎖;
- 使用synchronized修飾的方法都是可重入的。
synchronized的實現原理
monitorenter和monitorexit
將下面兩段程式碼分別用 javac *.java編譯成.class檔案,再反編譯 javap -verbose *.class檔案
public class SynchronizedThis {
public void method() {
synchronized(this) {}
}
}
// 反編譯結果
public void method();
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
複製程式碼
public class SynchronizedMethod {
public synchronized void method() {}
}
// 反編譯結果
public synchronized void method();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 2: 0
複製程式碼
可以看到:
- synchronized加在程式碼塊上,JVM是通過monitorenter和monitorexit來控制鎖的獲取的釋放的;
- synchronized加在方法上,JVM是通過ACC_SYNCHRONIZED標記來控制的,但本質上也是通過monitorenter和monitorexit指令控制的。
物件頭
上面我們提到monitor,這是什麼鬼? 其實,物件在記憶體是這樣儲存的,包括物件頭、例項資料和對齊填充Padding,其中物件頭包括 Mark Word和型別指標。
Mark Word
Mark Word用於儲存物件自身的執行時資料,如雜湊碼(identity_hashcode)、GC分代年齡(age)、鎖狀態標誌(lock)、執行緒持有的鎖、偏向執行緒ID(thread)、偏向時間戳(epoch)等等,佔用記憶體大小與虛擬機器位長一致。
Mark Word (32 bits) | State 鎖狀態 |
---|---|
identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 | Normal 無鎖 |
thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 | Biased 偏向鎖 |
ptr_to_lock_record:30 | lock:2 | Lightweight Locked 輕量級鎖 |
ptr_to_heavyweight_monitor:30 | lock:2 | Heavyweight Locked 重量級鎖 |
| lock:2 | Marked for GC GC標記 |
Mark Word (64 bits) | State 鎖狀態 |
---|---|
unused:25|identity_hashcode:31|unused:1|age:4|biased_lock:1|lock:2 | Normal 無鎖 |
thread:54 |epoch:2|unused:1|age:4|biased_lock:1|lock:2 | Biased 偏向鎖 |
ptr_to_lock_record:62 | lock:2 | Lightweight Locked 輕量級鎖 |
ptr_to_heavyweight_monitor:62 | lock:2 | Heavyweight Locked 重量級鎖 |
| lock:2 | Marked for GC GC標記 |
可以看到,monitor就存在Mark Word中。
型別指標
型別指標指向物件的類後設資料metadata,虛擬機器通過這個指標確定該物件是哪個類的例項。
鎖狀態
biased_lock | lock | 狀態 |
---|---|---|
0 | 01 | 無鎖 |
1 | 01 | 偏向鎖 |
0 | 00 | 輕量級鎖 |
0 | 10 | 重量級鎖 |
0 | 11 | GC標記 |
JDK對synchronized的優化
jdk1.6之前synchronized是很重的,所以並不被開發者偏愛,隨著後續版本jdk對synchronized的優化使其越來越輕量,它還是很好用的,甚至ConcurrentHashMap在jdk的put方法都在jdk1.8時從ReetrantLock.tryLock()改為用synchronized來實現同步。 並且還引入了偏向鎖,輕量級鎖等概念,下面是偏向鎖和輕量級鎖的獲取流程
www.processon.com/diagraming/…
參考 《咕泡學院公開課》 提取碼:s6vx
偏向鎖 baised_lock
如果一個執行緒獲取了偏向鎖,那麼如果在接下來的一段時間裡,如果沒有其他執行緒來搶佔鎖,那麼獲取鎖的執行緒在下一次進入方法時不需要重新獲取鎖。
synchronized與ReentrantLock的區別
區別 | synchronized | ReentrantLock |
---|---|---|
靈活性 | 程式碼簡單,自動獲取、釋放鎖 | 相對繁瑣,需要手動獲取、釋放鎖 |
是否可重入 | 是 | 是 |
作用位置 | 可作用在方法和程式碼塊 | 只能用在程式碼塊 |
獲取、釋放鎖的方式 | monitorenter、monitorexit、ACC_SYNCHRONIZED | 嘗試非阻塞獲取鎖tryLock()、超時獲取鎖tryLock(long timeout,TimeUnit unit)、unlock() |
獲取鎖的結果 | 不知道 | 可知,tryLock()返回boolean |
使用注意事項 | 1、鎖物件不能為空(鎖儲存在物件頭中,null沒有物件頭) 2、作用域不宜過大 |
1、切記要在finally中unlock(),否則會形成死鎖 2、不要將獲取鎖的過程寫在try塊內,因為如果在獲取鎖時發生了異常,異常丟擲的同時,也會導致鎖無故被釋放。 |