- Posted by 微博@Yangsc_o
- 原創文章,版權聲明:自由轉載-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0
文章目錄
摘要
本文從synchronized的基本用法,結合對象的內存佈局,深入探究synchronized的鎖升級的過程,探究其實現原理。準備開始~
synchronized的基礎用法
-
修飾普通同步⽅方法
對於非static的情況,synchronized是對象級別的,其實質是將synchronized作用於對象引用(object reference)上,即拿到p1對象鎖的線程,對的fun()方法有同步互斥作用,不同的對象之間堅持“和平共處”。
// 鎖的對象是方法的調用者 public synchronized void method(){ }
上邊的示例代碼等同於如下代碼:
public void method() { synchronized (this) { } }
-
修飾靜態同步方法
如果方法用static修飾,synchronized的作用範圍就是class一級的,它對類的所有對象起作用。
Class Foo{ public static synchronized void method1() { } }
上邊的示例代碼等同於如下代碼:
public void method2() { synchronized(Foo.class) { // class literal(類名稱字面常量) //請注意,Foo.class也是一個對象,類型是Class,在一個ClassLoader裏,它是唯一的。 } }
-
修飾同步代碼塊
鎖就是so這個對象,誰拿到這個鎖誰就能夠運行他所控制的那段代碼。
同步塊,示例代碼如下:
public void method(SomeObject so) { synchronized(so) { //….. } }
當有一個明確的對象作爲鎖時,就能夠這樣寫代碼,但當沒有明確的對象作爲鎖,只是想讓一段代碼同步時,能夠創建一個特別的instance變量(它得是個對象)來充當鎖
class Foo implements Runnable{ private byte[] lock = new byte[0]; // 特別的instance變量 Public void method(){ synchronized(lock) { } }
- 注:零長度的byte數組對象創建起來將比任何對象都經濟。查看編譯後的字節碼:生成零長度的byte[]對象只需3條操作碼,而Object lock = new Object()則需要7行操作碼。
特殊說明
- 使⽤用synchronized修飾類和對象時,由於類對象和實例例對象分別擁有⾃自⼰己的監視器器鎖,因此不不會
相互阻塞 - 使⽤synchronized修飾實例例對象時,如果一個線程正在訪問實例例對象的一個synchronized方法時,其它線程不僅不能訪問該synchronized⽅法,該對象的其它synchronized方法也不不能訪問,因爲一個對象只有一個監視器器鎖對象,但是其它線程可以訪問該對象的非synchronized⽅法。
synchronized原理
字節碼理解
在說synchronized原理時,就不得不先了解一下Monitor
認識 Java Monitor Object
Java Monitor 從兩個方面來支持線程之間的同步,即:互斥執行(對象內的所有方法都互斥的執行。好比一個 Monitor 只有一個運行許可,任一個線程進入任何一個方法都需要獲得這個許可,離開時把許可歸還)與協作(通常提供signal機制:允許正持有許可的線程暫時放棄許可,等待某個監視條件成真,條件成立後,當前線程可以通知正在等待這個條件的線程,讓它可以重新獲得運行許可)。 Java 使用對象鎖 ( 使用 synchronized 獲得對象鎖 ) 保證工作在共享的數據集上的線程互斥執行 , 使用 notify/notifyAll/wait 方法來協同不同線程之間的工作。這些方法在 Object 類上被定義,會被所有的 Java 對象自動繼承。
Java 語言對於這樣一個典型併發設計模式做了內建的支持,線程如果獲得監視鎖成功,將成爲該監視者對象的擁有者。在任一時刻內,監視者對象只屬於一個活動線程 (Owner) 。擁有者線程可以調用 wait 方法自動釋放監視鎖,進入等待狀態。下圖很好地描述了 Java Monitor 的工作機理。
在Java虛擬機(HotSpot)中, monitor是由ObjectMonitor實現的,其主要數據結構如下(位於HotSpot虛擬機源碼ObjectMonitor.hpp⽂文件,C++實現的),位於/openjdk-8u60/hotspot/src/share/vm/runtime/objectMonitor.hpp
ObjectMonitor() {
_header = NULL; //markOop對象頭
_count = 0; // 可以⼤大於1,可重⼊入
_waiters = 0, //等待線程數
_recursions = 0; //重⼊入次數
_object = NULL; //監視器器鎖寄⽣生的對象。鎖不不是平⽩白出現的,⽽而是寄託存儲於對象中。
_owner = NULL; //初始時爲NULL表示當前沒有任何線程擁有該monitor record,當線程成功擁有該鎖後保存線程唯⼀一標識,當鎖被釋放時⼜又設置爲NULL
_WaitSet = NULL; //處於wait狀態的線程,會被加⼊入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //處於等待鎖block狀態的線程,會被加⼊入到該列列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
每個線程都有兩個ObjectMonitor對象列列表,分別爲free和used列列表,如果當前free列列表爲空,線程將
向全局global list請求分配ObjectMonitor。thread結構/openjdk-8u60/hotspot/src/share/vm/runtime/thread.hpp
// Private thread-local objectmonitor list - a simple cache organized as a SLL.
public:
ObjectMonitor* omFreeList;
int omFreeCount; // length of omFreeList
int omFreeProvision; // reload chunk size
ObjectMonitor* omInUseList; // SLL to track monitors in circulation
int omInUseCount; // length of omInUseList
ObjectMonitor中有兩個隊列列, _WaitSet 和 _EntryList,用來保存ObjectWaiter對象列列表( 每個等待鎖的線程都會被封裝成ObjectWaiter對象), owner指向持有ObjectMonitor對象的線程,當多個線程同時訪問一段同步代碼時,首先會進⼊入 EntryList 集合,當線程獲取到對象的monitor 後進⼊ _Owner 區域並把monitor中的owner變量量設置爲當前線程同時monitor中的計數器器count加1,若線程調⽤用 wait() ⽅法,將釋放當前持有的monitor, owner變量量恢復爲null, count⾃自減1,同時該線程進⼊入 WaitSet集合中等待被喚醒。若當前線程執⾏行行完畢也將釋放monitor(鎖)並復位變量量的值,以便便其他線程進⼊入獲取monitor(鎖)。
由此看來, monitor對象存在於每個Java對象的對象頭中(存儲的指針的指向), synchronized鎖便是通過這種⽅式獲取鎖的,也是爲什麼Java中任意對象可以作爲鎖的原因。
代碼驗證
是不是一臉崩?不知道在說什麼🤷♂️?
-
先看一張圖
概括一下就是synchronized後,在程序執行的過程中會有被監聽的過程,過程可以概括爲:獲取監聽器、dosomething、釋放監聽器
- 測試驗證代碼
public class ObjectLayout {
public static synchronized void test(){
}
public synchronized void test1(){
}
public static void main(String[] args) {
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj){
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
ObjectLayout.test();
ObjectLayout objectLayout = new ObjectLayout();
objectLayout.test1();
}
}
- 編譯爲字節碼
0 new #2 <java/lang/Object>
3 dup
4 invokespecial #1 <java/lang/Object.<init>>
7 astore_1
8 getstatic #3 <java/lang/System.out>
11 aload_1
12 invokestatic #4 <org/openjdk/jol/info/ClassLayout.parseInstance>
15 invokevirtual #5 <org/openjdk/jol/info/ClassLayout.toPrintable>
18 invokevirtual #6 <java/io/PrintStream.println>
21 aload_1
22 dup
23 astore_2
24 monitorenter
25 getstatic #3 <java/lang/System.out>
28 aload_1
29 invokestatic #4 <org/openjdk/jol/info/ClassLayout.parseInstance>
32 invokevirtual #5 <org/openjdk/jol/info/ClassLayout.toPrintable>
35 invokevirtual #6 <java/io/PrintStream.println>
38 aload_2
39 monitorexit
40 goto 48 (+8)
43 astore_3
44 aload_2
45 monitorexit
46 aload_3
47 athrow
48 invokestatic #7 <com/yangsc/juc/ObjectLayout.test>
51 new #8 <com/yangsc/juc/ObjectLayout>
54 dup
55 invokespecial #9 <com/yangsc/juc/ObjectLayout.<init>>
58 astore_2
59 aload_2
60 invokevirtual #10 <com/yangsc/juc/ObjectLayout.test1>
63 return
可以看到,在synchronized (obj){}被monitorenter和moniterexit包裹了!
-
靜態同步⽅法&同步⽅法
字節碼中會標記爲volatile,後續交由虛擬機處理;
虛擬機實現原理
一個對象的內存佈局
在更進一步的深入之前,我們需要了解一下Java對象在內存的佈局,一個Java對象包括對象頭、實例數據和補齊填充3個部分:
對象頭
- Mark Word:包含一系列的標記位,比如輕量級鎖的標記位,偏向鎖標記位等等。在32位系統佔4字節,在64位系統中佔8字節;
- Class Pointer:用來指向對象對應的Class對象(其對應的元數據對象)的內存地址。在32位系統佔4字節,在64位系統中佔8字節;
- Length:如果是數組對象,還有一個保存數組長度的空間,佔4個字節;
對象實際數據
對象實際數據包括了對象的所有成員變量,其大小由各個成員變量的大小決定,比如:byte和boolean是1個字節,short和char是2個字節,int和float是4個字節,long和double是8個字節,reference是4個字節(64位系統中是8個字節)。
對於reference類型來說,在32位系統上佔用4bytes, 在64位系統上佔用8bytes。
對齊填充
Java對象佔用空間是8字節對齊的,即所有Java對象佔用bytes數必須是8的倍數。例如,一個包含兩個屬性的對象:int和byte,這個對象需要佔用8+4+1=13個字節,這時就需要加上大小爲3字節的padding進行8字節對齊,最終佔用大小爲16個字節。
注意:以上對64位操作系統的描述是未開啓指針壓縮的情況,關於指針壓縮會在下文中介紹。
對象頭佔用空間大小
這裏說明一下32位系統和64位系統中對象所佔用內存空間的大小:
- 在32位系統下,存放Class Pointer的空間大小是4字節,MarkWord是4字節,對象頭爲8字節;
- 在64位系統下,存放Class Pointer的空間大小是8字節,MarkWord是8字節,對象頭爲16字節;
- 64位開啓指針壓縮的情況下,存放Class Pointer的空間大小是4字節,
MarkWord
是8字節,對象頭爲12字節; - 如果是數組對象,對象頭的大小爲:數組對象頭8字節+數組長度4字節+對齊4字節=16字節。其中對象引用佔4字節(未開啓指針壓縮的64位爲8字節),數組
MarkWord
爲4字節(64位未開啓指針壓縮的爲8字節); - 靜態屬性不算在對象大小內。
指針壓縮
從上文的分析中可以看到,64位JVM消耗的內存會比32位的要多大約1.5倍,這是因爲對象指針在64位JVM下有更寬的尋址。對於那些將要從32位平臺移植到64位的應用來說,平白無辜多了1/2的內存佔用,這是開發者不願意看到的。
從JDK 1.6 update14開始,64位的JVM正式支持了 -XX:+UseCompressedOops 這個可以壓縮指針,起到節約內存佔用的新參數。
什麼是OOP?
OOP的全稱爲:Ordinary Object Pointer,就是普通對象指針。啓用CompressOops後,會壓縮的對象:
- 每個Class的屬性指針(靜態成員變量);
- 每個對象的屬性指針;
- 普通對象數組的每個元素指針。
當然,壓縮也不是所有的指針都會壓縮,對一些特殊類型的指針,JVM是不會優化的,例如指向PermGen的Class對象指針、本地變量、堆棧元素、入參、返回值和NULL指針不會被壓縮。
啓用指針壓縮
在Java程序啓動時增加JVM參數:-XX:+UseCompressedOops
來啓用。
注意:32位HotSpot VM是不支持UseCompressedOops參數的,只有64位HotSpot VM才支持。
本文中使用的是JDK 1.8,默認該參數就是開啓的。
扯了半天一個java對象在內存的佈局,其實針對此文,重點需要關注markword
我們也可以在jdk的’/openjdk-8u60/hotspot/src/share/vm/memory/universe.hpp’中看到相關描述
// Bit-format of an object header (most significant first, big endian layout below):
//
// 32 bits:
// --------
// hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
// size:32 ------------------------------------------>| (CMS free block)
// PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
// 64 bits:
// --------
// unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
// PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
// size:64 ----------------------------------------------------->| (CMS free block)
//
// unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object)
// JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object)
// narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
// unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
//
------------------省略部分描述------------------
// [JavaThread* | epoch | age | 1 | 01] lock is biased toward given thread
// [0 | epoch | age | 1 | 01] lock is anonymously biased
//
// - the two lock bits are used to describe three states: locked/unlocked and monitor.
//
// [ptr | 00] locked ptr points to real header on stack
// [header | 0 | 01] unlocked regular object header
// [ptr | 10] monitor inflated lock (header is wapped out)
// [ptr | 11] marked used by markSweep to mark an object
// not valid at any other time
//
// We assume that stack/thread pointers have the lowest two bits cleared.
鎖升級的過程:無鎖 - 偏向鎖 -輕量級鎖(自旋鎖)-重量級鎖
-
偏向鎖: markword 上記錄當前線程指針,下次同一個線程加鎖的時候,不需要爭用,只需要判斷線程指針是否同一個,所以,偏向鎖,偏向加鎖的第一個線程 。hashCode備份在線程棧上 線程銷燬,鎖降級爲無鎖
-
輕量級鎖:有爭用 - 鎖升級爲輕量級鎖 - 每個線程有自己的LockRecord在自己的線程棧上,用CAS去爭用markword的LR的指針,指針指向哪個線程的LR,哪個線程就擁有鎖
-
重量級鎖:自旋超過10次(舊版本),升級爲重量級鎖 - 如果太多線程自旋 CPU消耗過大,不如升級爲重量級鎖,進入等待隊列(不消耗CPU)-XX:PreBlockSpin
自旋鎖在 JDK1.4.2 中引入,使用 -XX:+UseSpinning 來開啓。JDK 6 中變爲默認開啓,並且引入了自適應的自旋鎖(適應性自旋鎖)。
自適應自旋鎖意味着自旋的時間(次數)不再固定,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,並且持有鎖的線程正在運行中,那麼虛擬機就會認爲這次自旋也是很有可能再次成功,進而它將允許自旋等待持續相對更長的時間。如果對於某個鎖,自旋很少成功獲得過,那在以後嘗試獲取這個鎖時將可能省略掉自旋過程,直接阻塞線程,避免浪費處理器資源。
偏向鎖由於有鎖撤銷的過程revoke,會消耗系統資源,所以,在鎖爭用特別激烈的時候,用偏向鎖未必效率高。還不如直接使用輕量級鎖。
測試驗證
public class ObjectLayout1 {
public static void main(String[] args) {
MyObj obj = new MyObj();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj) {
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
for (int i = 0; i < 100; i++) {
new Thread(()->{
obj.add();
}).start();
}
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
class MyObj {
private Integer count = 0;
public synchronized void add() {
count++;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
-
不加鎖
00000001 對應無鎖狀態
com.yangsc.juc.MyObj object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 6e 37 02 f8 (01101110 00110111 00000010 11111000) (-134072466) 12 4 java.lang.Integer MyObj.count 0 Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
-
偏向鎖
10010000 對應輕量級標誌位
com.yangsc.juc.MyObj object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 90 69 30 0c (10010000 01101001 00110000 00001100) (204499344) 4 4 (object header) 00 70 00 00 (00000000 01110000 00000000 00000000) (28672) 8 4 (object header) 6e 37 02 f8 (01101110 00110111 00000010 11111000) (-134072466) 12 4 java.lang.Integer MyObj.count 0 Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
-
重量級鎖
01111010 對應重量級鎖標誌位
com.yangsc.juc.MyObj object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 7a a8 80 4e (01111010 10101000 10000000 01001110) (1317054586) 4 4 (object header) ba 7f 00 00 (10111010 01111111 00000000 00000000) (32698) 8 4 (object header) 6e 37 02 f8 (01101110 00110111 00000010 11111000) (-134072466) 12 4 java.lang.Integer MyObj.count 1 Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
上面的markword驗證鎖升級的過程,但是Hotspot是如何實現呢?
翻開虛擬機源碼位置:openjdk-8u60/hotspot/src/share/vm/interpreter/interpreterRuntime.cpp
函數:InterpreterRuntime:: monitorenter
注:JDK 1.6中默認開啓偏向鎖,可以通過-XX:-UseBiasedLocking來禁用偏向鎖。
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
if (PrintBiasedLockingStatistics) {
Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
}
Handle h_obj(thread, elem->obj());
assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
"must be NULL or an object");
if (UseBiasedLocking) {
// 如果 UseBiasedLocking 開啓了了偏向鎖,優先獲得的是偏向鎖,否則是輕量量級鎖。
// Retry fast entry if bias is revoked to avoid unnecessary inflation
ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
} else {
ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
}
assert(Universe::heap()->is_in_reserved_or_null(elem->obj()),
"must be NULL or an object");
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END
- 獲取偏向鎖入口fast_enter
// -----------------------------------------------------------------------------
// Fast Monitor Enter/Exit
// This the fast monitor enter. The interpreter and compiler use
// some assembly copies of this code. Make sure update those code
// if the following function is changed. The implementation is
// extremely sensitive to race condition. Be careful.
void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
// 開啓了了偏向鎖
if (UseBiasedLocking) {
// 沒有到達安全點
if (!SafepointSynchronize::is_at_safepoint()) {
// 獲取偏向鎖
BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
return;
}
} else {
// 釋放偏向鎖
assert(!attempt_rebias, "can not rebias toward VM thread");
BiasedLocking::revoke_at_safepoint(obj);
}
assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
}
// 獲取輕量量級鎖
slow_enter (obj, lock, THREAD) ;
}
偏向鎖的撤銷
只有當其它線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖,偏向鎖的撤銷由BiasedLocking::revoke_at_safepoint
方法實現:
-
revoke_and_rebias
偏向鎖獲取的具體邏輯 /hotspot/src/share/vm/runtime/synchronizer.cpp, 偏向鎖的獲取由 BiasedLocking::revoke_and_rebias ⽅法實現,這個方法太長了,有興趣的看一下吧;
-
輕量級鎖
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) { markOop mark = obj->mark(); assert(!mark->has_bias_pattern(), "should not see bias pattern here"); if (mark->is_neutral()) { lock->set_displaced_header(mark); if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) { TEVENT (slow_enter: release stacklock) ; return ; } } else if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) { assert(lock != mark->locker(), "must not re-lock the same lock"); assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock"); lock->set_displaced_header(NULL); return; } #if 0 if (mark->has_monitor() && mark->monitor()->is_entered(THREAD)) { lock->set_displaced_header (NULL) ; return ; } #endif lock->set_displaced_header(markOopDesc::unused_mark()); ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD); }
輕量級鎖的釋放通過
ObjectSynchronizer::fast_exit
完成。
重量級鎖
重量級鎖通過對象內部的監視器(monitor)實現,其中monitor的本質是依賴於底層操作系統的Mutex Lock實現,操作系統實現線程之間的切換需要從用戶態到內核態的切換,切換成本非常高。
鎖膨脹過程,鎖的膨脹過程通過ObjectSynchronizer::inflate
函數實現,代碼也非常的長,此處省略,有興趣的可以讀一下。
鎖消除 lock eliminate
public void add(String str1,String str2){
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}
我們都知道 StringBuffer 是線程安全的,因爲它的關鍵方法都是被 synchronized 修飾過的,但我們看上面這段代碼,我們會發現,sb 這個引用只會在 add 方法中使用,不可能被其它線程引用(因爲是局部變量,棧私有),因此 sb 是不可能共享的資源,JVM 會自動消除 StringBuffer 對象內部的鎖。
鎖粗化 lock coarsening
public String test(String str){
int i = 0;
StringBuffer sb = new StringBuffer():
while(i < 100){
sb.append(str);
i++;
}
return sb.toString():
}
JVM 會檢測到這樣一連串的操作都對同一個對象加鎖(while 循環內 100 次執行 append,沒有鎖粗化的就要進行 100 次加鎖/解鎖),此時 JVM 就會將加鎖的範圍粗化到這一連串的操作的外部(比如 while 虛幻體外),使得這一連串操作只需要加一次鎖即可。
總而言之,synchronized爲了進⼀一步提升synchronized的性能,提⾼高多線程環境下的併發效率,做了很多努力!
自此,synchronized回顧完畢。