[TOC]
概述
Java 在語法層面提供了 synchronized 關鍵字來實現多線程同步,雖然 Java 有 ReentrantLock 等高級鎖,但是 synchronized 用法簡單,不易出錯,并且 JDK6 對其進行了諸多優化,性能也不差,故而依然值得我們去使用。
本文,我們將對 synchronized 對實現進行剖析,分析其實現原理以及 JDK6 引入了哪些鎖優化對手段。
synchronized 實現
我們先看一段代碼:
public class LockTest {
public synchronized void testSync() {
System.out.println("testSync");
}
public void testSync2() {
synchronized(this) {
System.out.println("testSync2");
}
}
}
在這段代碼中,分布使用 synchronized 對方法和語句塊進行了同步,接下來我們使用 javac 編譯后,再用 javap 命令查看其匯編代碼:
javap -verbose LockTest.class
public synchronized void testSync();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String testSync
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 9: 0
line 10: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/qunar/fresh2017/LockTest;
public void testSync2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #5 // String testSync2
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
13: monitorexit
14: goto 22
17: astore_2
18: aload_1
19: monitorexit
20: aload_2
21: athrow
22: return
Exception table:
from to target type
4 14 17 any
17 20 17 any
LineNumberTable:
line 13: 0
line 14: 4
line 15: 12
line 16: 22
LocalVariableTable:
Start Length Slot Name Signature
0 23 0 this Lcom/qunar/fresh2017/LockTest;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 17
locals = [ class com/qunar/fresh2017/LockTest, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
這里我們省略了關鍵方法之外對常量池、構造函數等部分,對于 synchronized 方法,僅僅在其 class 文件的 access_flags 字段中設置了 ACC_SYNCHRONIZED 標志。對于 synchronized 語句塊,分布在同步塊的入口和出口插入了 monitorenter 和 monitorexit 字節碼指令。
注意這里有多個 monitorexit ,除了在正常出口插入了 monitorexit,還在異常處理代碼里插入了 monitorexit(請看 Exception table )。
在這篇文章 OpenJDK9 Hotspot : synchronized 淺析里,可以看到 monitorenter 的處理邏輯位于 bytecodeInterpreter.cpp 中,其加鎖順序為偏向鎖 -> 輕量級鎖 -> 重量級鎖。最終重量級鎖的代碼位于 ObjectMonitor::enter 中,enter 調用了 EnterI 方法,EnterI 方法中,調用了線程擁有的 Parker 實例的 park 方法,這一點和 LockSupport 一致,畢竟都是需要底層操作系統支持的。
// in /vm/runtime/objectMonitor.cpp
void ATTR ObjectMonitor::enter(TRAPS) {
// omit a lot
for (;;) {
jt->set_suspend_equivalent();
// cleared by handle_special_suspend_equivalent_condition()
// or java_suspend_self()
EnterI (THREAD) ;
// omit a lot
}
}
void ATTR ObjectMonitor::EnterI (TRAPS) {
Thread * Self = THREAD ;
// 省略很多
for (;;) {
// omit a lot
// park self
if (_Responsible == Self || (SyncFlags & 1)) {
TEVENT (Inflated enter - park TIMED) ;
Self->_ParkEvent->park ((jlong) RecheckInterval) ;
// Increase the RecheckInterval, but clamp the value.
RecheckInterval *= 8 ;
if (RecheckInterval > 1000) RecheckInterval = 1000 ;
} else {
TEVENT (Inflated enter - park UNTIMED) ;
Self->_ParkEvent->park() ;
}
}
// omit a lot
鎖的粒度
鎖的粒度是一個很關鍵的問題,粒度的大小對于線程的并發性能有很大影響,比如數據庫中表鎖的并發度要比遠低于行鎖。下面介紹一下 synchronized 幾種常見用法中,鎖的粒度:
- 對于同步方法,鎖是當前實例對象。
- 對于靜態同步方法,鎖是當前對象的Class對象。
- 對于同步方法塊,鎖是 synchronized 括號里配置的對象。
鎖優化
JVM
JDK6 中為了提升鎖的性能,引入了“偏向鎖”和“輕量級鎖”的概念,所以在 Java 中鎖一共有 4 種狀態:無鎖、偏向鎖、輕量級鎖、重量級鎖。它會隨著競爭情況逐漸升級,鎖可以升級但不能降級,也就是說不能有重量級鎖變為輕量級鎖,也不能由輕量級鎖變為偏向鎖。下面我們介紹一下這幾種鎖的特點:
鎖 | 場景 | 優點 | 缺點 |
---|---|---|---|
偏向鎖 | 適用于只有一個線程訪問同步塊的場景 | 加鎖和解鎖不存在額外的消耗,和執行非同步方法比僅存在納秒級的差距 | 如果線程間存在競爭,會帶來額外的鎖撤銷的消耗 |
輕量級鎖 | 適用于同步塊執行速度非常快的場景 | 競爭線程不會阻塞,減少了線程切換消耗的時間 | 如果線程間競爭激烈,會導致過多的自旋,消耗 CPU |
重量級鎖 | 使用于同步款執行較慢,鎖占用時間較長的場景 | 競爭激烈時,消耗 CPU 較少 | 線程阻塞,會引起線程切換 |
可以說,沒有哪一種鎖絕對比另一種鎖好,各自都有其適合的場景。下面再簡單描述一下,這些鎖具體是如何實現的:
- 偏向鎖:對象頭的鎖標志位置為 1 表示當前是偏向鎖,另有 23bit 用來記錄當前線程 id。如果一個線程發現當前是無鎖狀態,會將鎖狀態改為偏向鎖。如果已經是偏向鎖,并且記錄的線程 id 和當前線程一致,則認為是獲得了鎖;否則升級鎖到輕量級鎖。
- 輕量級鎖:其本質是自旋鎖,也就是輪詢去獲取鎖,實際中功能會高級一點兒,有自適應自旋功能,所謂自適應也就是根據競爭激烈程度適當調整自旋次數,以及決定是否升級為重量級鎖。
- 重量級鎖:其底層實現通常就是 POSIX 中的 mutex 和 condition。
代碼
前面講了 JVM 中優化鎖的性能的一些方法,這里再擴展一下,在實際的代碼編寫過程中,使用鎖時,有哪些方法可以改進性能?
- 減少鎖占用時間:減少鎖占用時間,能過有效的減少競爭激烈程度,減少到一定程度,就可以使用輕量級鎖,代替重量級鎖來提升性能。如何減少呢?
- 減少不必要的占用鎖的代碼。
- 降低鎖的粒度。
- 鎖分離:該技術能過降低鎖占用時間,或者減少競爭激烈程度。
- 將一個大鎖分解多個小鎖,比如從 HashTable 的對象級別鎖到 ConcurrentHashMap 的分段鎖的優化。
- 按照同步操作的性質來拆分,比如讀寫鎖大大提升了讀操作的性能。
- 鎖粗化:看起來與鎖分離相反,但是它們適用的場景不同。在一個間隔性地需要執行同步語句的線程中,如果在不連續的同步塊間頻繁加鎖解鎖是很耗性能的,因此把加鎖范圍擴大,把這些不連續的同步語句進行一次性加鎖解鎖。雖然線程持有鎖的時間增加了,但是總體來說是優化了的。
- 鎖消除:根據代碼逃逸技術的分析,如果一段代碼中的數據不會逃逸出當前線程,那么可以認為這段代碼是線程安全的,不必加鎖。
總結
在實際中,鎖的各種優化技術是可以一起使用的。比如在減少鎖占用時間后,就可以使用自旋鎖代替重量級鎖提升性能。