JAVA併發程式設計入門篇,思考同步鎖Synchronized背後的實現哲學

多執行緒在概念上類似搶佔式多工處理,執行緒的合理使用能夠提升程式的處理能力,但是使用的同時也帶來了弊端,對於共享變數訪問就會產生安全性的問題。下面來看一個多執行緒訪問共享變數的例子:

public class ThreadSafty { private static int count = 0; public static void incr() { try { Thread。sleep(1); } catch (InterruptedException e) { e。printStackTrace(); } count ++; } public static void main(String[] args) throws InterruptedException { for (int i = 0 ; i < 1000; i++) { new Thread(()->{ ThreadSafty。incr(); },“threadSafty” + i)。start(); } TimeUnit。SECONDS。sleep(3); System。out。println(“執行結果是:” + count); }}

變數count的執行結果始終是小於等於1000的隨機數,因為執行緒的可見性和原子性。

一、多執行緒訪問的資料安全性

如何保證執行緒並行執行的資料安全性問題,這裡首先能夠想到的是加鎖吧。關係型資料庫中有樂觀鎖、悲觀鎖,那麼什麼是鎖呢?它是處理併發的一種手段,實現互斥的特性。

在Java語言中實現鎖的關鍵字是

Synchronized

二、Synchronized的基本應用

2。1 Synchronized的三種加鎖方式

靜態方法:作用於當前類物件加鎖,進入同步程式碼前要獲得當前類物件的鎖

synchronized static void method(){}

修飾程式碼塊:指定加鎖物件,進入同步程式碼前要獲得指定物件的鎖

void method(){ synchronized (SynchronizedDemo。class){}}

修改例項方法:作用於當前例項加鎖,進入同步程式碼前要獲得當前例項的鎖

Object lock = new Object(); //只針對於當前物件例項有效。 public SynchronizedDemo(Object lock){ this。lock = lock; } void method(){ synchronized(lock){} }

2。2 Synchronized鎖是如何儲存資料的呢?

以物件在

jvm

記憶體中是如何儲存作為切入點,去看看物件裡面有什麼特效能夠實現鎖的

2。2。1 物件在Heap記憶體中的佈局

在Hotspot虛擬機器中,物件在堆記憶體中的佈局,可以分為三個部分:

物件頭:包括物件標記、類元資訊

例項資料

對齊填充

JAVA併發程式設計入門篇,思考同步鎖Synchronized背後的實現哲學

Hotspot

採用

instanceOopDesc

arrayOopDesc

來描述物件頭,

arrayOopDesc

物件用來描述陣列型別的。

instanceOopDesc

的定義在Hotspot原始碼中的

instanceOop.hpp

檔案中,另外,

arrayOopDesc

的定義對應

arrayOop.hpp

class instanceOopDesc : public oopDesc { public: // aligned header size。 static int header_size() { return sizeof(instanceOopDesc)/HeapWordSize; } // If compressed, the offset of the fields of the instance may not be aligned。 static int base_offset_in_bytes() { // offset computation code breaks if UseCompressedClassPointers // only is true return (UseCompressedOops && UseCompressedClassPointers) ? klass_gap_offset_in_bytes() : sizeof(instanceOopDesc); } static bool contains_field_offset(int offset, int nonstatic_field_size) { int base_in_bytes = base_offset_in_bytes(); return (offset >= base_in_bytes && (offset-base_in_bytes) < nonstatic_field_size * heapOopSize); }};#endif // SHARE_VM_OOPS_INSTANCEOOP_HPP

看原始碼instanceOopDesc繼承自oopDesc,oopDesc定義在oop。hpp檔案中:

class oopDesc { friend class VMStructs; private: volatile markOop _mark; union _metadata { Klass* _klass;//普通指標 narrowKlass _compressed_klass;//壓縮類指標 } _metadata; // Fast access to barrier set。 Must be initialized。 static BarrierSet* _bs;……

在oopDesc類中有兩個重要的成員變數,_mark:記錄物件和鎖有關的資訊,屬於markOop型別,_metadata:記錄類元資訊

class markOopDesc: public oopDesc { private: // Conversion uintptr_t value() const { return (uintptr_t) this; } public: // Constants enum { age_bits = 4,//分代年齡 lock_bits = 2,//鎖標識 biased_lock_bits = 1,//是否為偏向鎖 max_hash_bits = BitsPerWord - age_bits - lock_bits - biased_lock_bits, hash_bits = max_hash_bits > 31 ? 31 : max_hash_bits,//物件的hashCode cms_bits = LP64_ONLY(1) NOT_LP64(0), epoch_bits = 2//偏向鎖的時間戳 };……

markOopDesc

記錄了物件和鎖有關的資訊,也就是我們常說的

Mark Word

,當某個物件加上

Synchronized

關鍵字時,那麼和鎖有關的一系列操作都與它有關。

32位

系統

Mark Word

的長度是

32bit

64位

系統則是

64bit

Mark Word

裡面的資料會隨著鎖的標誌位的變化而變化的。

JAVA併發程式設計入門篇,思考同步鎖Synchronized背後的實現哲學

2。2。2 Java中列印物件的佈局

pom依賴

org。openjdk。jol jol-core 0。10

System。out。println(ClassLayout。parseInstance(synchronizedDemo)。toPrintable());

com。sy。sa。thread。SynchronizedDemo object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 31 00 00 00 (00110001 00000000 00000000 00000000) (49) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315) 12 4 (loss due to the next object alignment)Instance size: 16 bytesSpace losses: 0 bytes internal + 4 bytes external = 4 bytes total

大端儲存和小端儲存

0 4 (object header) 31 00 00 00 (00110001 00000000 00000000 00000000) (49) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)

16進位制: 0x 00 00 00 00 00 00 00 01(64位)2進位制: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000 0

0 01 (無鎖狀態)

透過最後三位來看鎖的狀態和標記。

OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) a8 f7 76 02 (10101000 11110111 01110110 00000010) (41351080) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315) 12 4 (loss due to the next object alignment)Instance size: 16 bytesSpace losses: 0 bytes internal + 4 bytes external = 4 bytes total

000表示為輕量級鎖

2。2。3 為什麼什麼物件都能實現鎖?

Java

中的每個物件都派生自

Object

類,而每個

Java Object

JVM

內部都有一個

native

的 C++物件

oop/oopDesc

進行對應。

執行緒在獲取鎖的時候,實際上就是獲得一個監視器物件(

monitor

) ,

monitor

可以認為是一個同步物件,所有的Java 物件是天生攜帶

monitor

。在

hotspot

原始碼的

markOop.hpp

檔案中,可以看到下面這段程式碼:

ObjectMonitor* monitor() const { assert(has_monitor(), “check”); // Use xor instead of &~ to provide one extra tag-bit check。 return (ObjectMonitor*) (value() ^ monitor_value); }

多個執行緒訪問同步程式碼塊時,相當於去爭搶物件監視器修改物件中的鎖標識,上面的程式碼中

ObjectMonitor

這個物件和執行緒爭搶鎖的邏輯有密切的關係。

2。3 Synchronized的鎖升級

鎖的狀態有:

無鎖、偏向鎖、輕量級鎖、重量級鎖。

鎖的狀態根據競爭激烈程度從低到高不斷升級。

2。3。1 偏向鎖

儲存(以32位為例):執行緒ID(23bit) Epoch(2bit) age(4bit) 是否偏向鎖(1bit) 鎖標誌位(2bit)

當一個執行緒加入了Synchronized同步鎖之後,會在物件頭(Object Header)儲存執行緒ID,後續這個執行緒進入或者退出這個同步程式碼塊的程式碼時,不需要再次加入和釋放鎖,而是直接比較物件頭裡面是否儲存了指向當前執行緒的偏向鎖。如果執行緒ID相等,就表示偏向鎖偏向於當前執行緒,就不需要再重新獲得鎖了。

com。sy。sa。thread。ClassLayoutDemo object internals:OFFSET SIZE  TYPE DESCRIPTION                VALUE  0   4    (object header)              05 e8 45 03(00000101 11101000 01000101 00000011) (54913029)  4   4    (object header)              00 00 00 00(00000000 00000000 00000000 00000000) (0)  8   4    (object header)              05 c1 00 f8(00000101 11000001 00000000 11111000) (-134168315)  12   4    (loss due to the next object alignment)Instance size: 16 bytesSpace losses: 0 bytes internal + 4 bytes external = 4 bytes total

JAVA併發程式設計入門篇,思考同步鎖Synchronized背後的實現哲學

2。3。2 輕量級鎖

儲存(以32位為例):指向棧中鎖記錄的指標(30bit) 鎖標誌位(2bit)

如果偏向鎖關閉或者當前偏向鎖指向其它的執行緒,那麼這個時候有執行緒去搶佔鎖,那麼將升級為輕量級鎖。

輕量級鎖在加鎖的過程中使用了自旋鎖,JDK1。6之後使用了自適應的自旋鎖。

JAVA併發程式設計入門篇,思考同步鎖Synchronized背後的實現哲學

2。3。3 重量級鎖

儲存(以32位為例):指向互斥量(重量級鎖)的指標(30bit) 鎖標誌位(2bit)

當輕量級鎖膨脹為重量級鎖後,執行緒只能被掛起阻塞等待被喚醒了。先來看一個重量級鎖的程式碼:

public class HeavyweightLock { public static void main(String[] args) { HeavyweightLock heavyweightLock = new HeavyweightLock(); Thread t1 = new Thread(()->{ synchronized (heavyweightLock) { System。out。println(“tl lock”); System。out。println(ClassLayout。parseInstance(heavyweightLock)。toPrintable()); } },“heavyheightLock”); t1。start(); synchronized (heavyweightLock) { System。out。println(“main lock”); System。out。println(ClassLayout。parseInstance(heavyweightLock)。toPrintable()); } }}

執行後的結果:

com。sy。sa。thread。HeavyweightLock object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 2a cc e9 02 (00101010 11001100 11101001 00000010) (48876586) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315) 12 4 (loss due to the next object alignment)Instance size: 16 bytesSpace losses: 0 bytes internal + 4 bytes external = 4 bytes totaltl lockcom。sy。sa。thread。HeavyweightLock object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 2a cc e9 02 (00101010 11001100 11101001 00000010) (48876586) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315) 12 4 (loss due to the next object alignment)Instance size: 16 bytesSpace losses: 0 bytes internal + 4 bytes external = 4 bytes total

每一個

Java

物件都會與一個監視器

monitor

關聯,可以把它理解成

一把鎖

,當一個執行緒要執行用

Synchronized

修改的程式碼塊或者物件時,該執行緒最先獲取到的是

Synchronized

修飾物件的

monitor

重量級加鎖的基本流程:

JAVA併發程式設計入門篇,思考同步鎖Synchronized背後的實現哲學

monitorenter

表示去獲得一個物件監視器。

monitorexit

表示釋放

monitor

監視器的所有權,使得其他被阻塞的執行緒可以嘗試去獲得這個監視器。

2。3。4 鎖升級總結

偏向鎖

只有在第一次請求時採用CAS在鎖物件的標記中記錄當前執行緒的地址,在之後該執行緒再次進入同步程式碼塊時,不需要搶佔鎖,直接判斷執行緒ID即可,這種適用於鎖會被同一個執行緒多次搶佔的情況。

輕量級鎖

採用CAS操作,把鎖物件的標記欄位替換為一個指標指向當前執行緒棧幀中的LockRecord,該工件儲存鎖物件原本的標記欄位,它針對的是多個執行緒在不同時間段內申請同一把鎖的情況。

重量級鎖

會阻塞、和喚醒加鎖的執行緒,它適用於多個執行緒同時競爭同一把鎖的情況。

JAVA併發程式設計入門篇,思考同步鎖Synchronized背後的實現哲學