多執行緒在概念上類似搶佔式多工處理,執行緒的合理使用能夠提升程式的處理能力,但是使用的同時也帶來了弊端,對於共享變數訪問就會產生安全性的問題。下面來看一個多執行緒訪問共享變數的例子:
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虛擬機器中,物件在堆記憶體中的佈局,可以分為三個部分:
物件頭:包括物件標記、類元資訊
例項資料
對齊填充
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
裡面的資料會隨著鎖的標誌位的變化而變化的。
2。2。2 Java中列印物件的佈局
pom依賴
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
2。3。2 輕量級鎖
儲存(以32位為例):指向棧中鎖記錄的指標(30bit) 鎖標誌位(2bit)
如果偏向鎖關閉或者當前偏向鎖指向其它的執行緒,那麼這個時候有執行緒去搶佔鎖,那麼將升級為輕量級鎖。
輕量級鎖在加鎖的過程中使用了自旋鎖,JDK1。6之後使用了自適應的自旋鎖。
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
。
重量級加鎖的基本流程:
monitorenter
表示去獲得一個物件監視器。
monitorexit
表示釋放
monitor
監視器的所有權,使得其他被阻塞的執行緒可以嘗試去獲得這個監視器。
2。3。4 鎖升級總結
偏向鎖
只有在第一次請求時採用CAS在鎖物件的標記中記錄當前執行緒的地址,在之後該執行緒再次進入同步程式碼塊時,不需要搶佔鎖,直接判斷執行緒ID即可,這種適用於鎖會被同一個執行緒多次搶佔的情況。
輕量級鎖
採用CAS操作,把鎖物件的標記欄位替換為一個指標指向當前執行緒棧幀中的LockRecord,該工件儲存鎖物件原本的標記欄位,它針對的是多個執行緒在不同時間段內申請同一把鎖的情況。
重量級鎖
會阻塞、和喚醒加鎖的執行緒,它適用於多個執行緒同時競爭同一把鎖的情況。