在 Java 中爲保證線程安全,可使用關鍵字 synchronized
保護代碼,在多個線程之間同時只能有一個線程執行被保護的代碼。html
synchronized
鎖的究竟是什麼?是對象,仍是代碼塊呢?java
保證線程安全已經有了 synchronized
爲何又會出現 Lock
呢,兩者之間有什麼區別呢?算法
synchronized
必定比 Lock
性能差嗎?安全
synchronized
的鎖升級過程是什麼,偏向鎖,輕量級鎖,自旋鎖,重量級鎖怎麼一步一步實現的?bash
一、用在靜態方法微信
public class SimpleUserSync { public static int a = 0; // 至關於 synchronized (SimpleUserSync.class){a++;} public synchronized static void addA_1() { a++; } }
二、用在成員方法上多線程
public class SimpleUserSync { public static int a = 0; // 至關於 synchronized (this){a++;} public synchronized void addA_1() { a++; } }
三、用在代碼塊oracle
private static final Object LOCK =new Object(); public static void addA_2() { synchronized (LOCK){ a++; } }
若是對一項技術只停留在會用的階段是遠遠不夠的,原理性的知識可避免掉到坑裏面去。佈局
JDK 1.6
以前,尚未進行 synchronized
的優化。那個時候 synchronized
只要申請鎖,java 進程
就會從 用戶態
切換到 內核態
,須要操做系統配合鎖定,這種切換相對來講比較佔用系統資源。性能
Lock
的實現的思想是:線程基於 CAS
操做在 用戶態
自旋改變內部的 state
,操做成功便可獲取鎖,操做不成功,繼續自旋獲取直到成功(分配的 cpu 時間執行完以後,再獲取到 cpu 資源,仍接着自旋獲取鎖)。這種實現方式在鎖競爭比較小的狀況下,效率是比較高的。比起 用戶態
切換到 內核態
,讓線程在哪裏自旋一會效率是比較高的。若是一直自旋(好比說 1 分鐘)獲取不到鎖,那用戶態
切換到 內核態
比你自旋一分鐘效率會高。
Lock
不必定比 synchronized
效率高,在鎖競爭的概率極大的狀況下,自旋消耗的資源遠大於 用戶態
切換到 內核態
佔用的資源。
JDK 1.6
對 synchronized
作了優化。在鎖競爭不大的狀況下,使用 偏向鎖
和 輕量級鎖
,這樣只用在 用戶態
完成鎖的申請。當鎖競爭的時候呢,會讓其自旋繼續獲取鎖,獲取 n 次仍是沒有獲取到(自適應自旋鎖),升級爲 重量級鎖
,用戶態
切換到 內核態
,從系統層級獲取鎖。
鎖升級的宏觀表現大體是這個樣子。自適應自旋鎖,自旋的次數 n,是 JVM
根據算法收集其自旋多少次獲取鎖算出來的(JDK 1.6 以後),是一個預測值,隨着數據收集愈來愈多,它也越準確。
synchronized
是經過鎖對象來實現的。所以瞭解一個對象的佈局,對咱們理解鎖的實現及升級是頗有幫助的。
<img src="http://oss.mflyyou.cn/blog/20200613211643.png?author=zhangpanqin" alt="image-20200613211643599" style="zoom: 25%;" />
對象填充,是將一個對象大小不足 8 個字節的倍數時,使用 0 填充補齊,爲了更高效效率的讀取數據,64 java 虛擬機,一次讀取是 64 bit(8 字節)。
在64位JVM上有一個壓縮指針選項-XX:+UseCompressedOops,默認是開啓的。開啓以後 Class Pointer
部分就會壓縮爲4字節,對象頭大小爲 12 字節
偏向鎖位
和 鎖標誌位
是鎖升級過程當中承擔重要的角色。
咱們可使用 jol
查看一個對象的對象頭信息,已達到觀測鎖升級的過程
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.10</version> </dependency>
public class JOLSample_01_Basic { public static void main(String[] args) throws Exception { out.println(ClassLayout.parseInstance(new JOLSample_01_Basic.A()).toPrintable()); } public static class A { boolean f; int a; } }
<font color=red>偏向鎖是默認開啓的,可是有個延遲時間</font>
# 查看偏向鎖配置的默認參數 java -XX:+PrintFlagsInitial | grep -i biased # 偏向鎖啓動的延遲,Java 虛擬機啓動 4 秒以後,建立的對象纔是匿名偏向,不然是普通對象 #intx BiasedLockingStartupDelay = 4000 {product} # 默認開啓偏向鎖 #bool UseBiasedLocking = true {product}
<font color=red>鎖升級以後,用戶線程不能降級。GC 線程能夠降級</font>
public class JOLSample_12_ThinLocking { public static void main(String[] args) throws Exception { A a = new A(); ClassLayout layout = ClassLayout.parseInstance(a); out.println("**** 對象建立,沒有通過鎖競爭"); out.println(layout.toPrintable()); synchronized (a) { out.println("**** 獲取到鎖"); out.println(layout.toPrintable()); } out.println("**** 鎖釋放"); out.println(layout.toPrintable()); } public static class A { } }
由於偏向鎖的延遲,建立的對象爲普通對象(偏向鎖位 0,鎖標誌位 01),獲取鎖的時候,無鎖
(偏向鎖位 0,鎖標誌位 01) 升級爲 輕量級鎖
(偏向鎖位 0,鎖標誌位 00),釋放鎖以後,對象的鎖信息(偏向鎖位 0,鎖標誌位 01)
<img src="http://oss.mflyyou.cn/blog/20200613221547.png?author=zhangpanqin" alt="image-20200613221547109" style="zoom: 33%;" />
synchronized (a)
的時候,由 a
的 Mark Word
中鎖偏向 0,鎖標誌位 01 知道鎖要升級爲輕量級鎖。java 虛擬機會在當前的線程的棧幀中創建一個鎖記錄(Lock Record)空間,Lock Record 儲存鎖對象的 Mark World
拷貝和當前鎖對象的指針。
java 虛擬機,使用 CAS
將 a 的 Mark Word(62 位)
指向當前線程(main 線程)中 Lock Record
指針,CAS 操做成功,將 a 的鎖標誌位變爲 00。
<img src="http://oss.mflyyou.cn/blog/20200613224235.png?author=zhangpanqin" alt="image-20200613224235023" style="zoom:50%;" />
CAS 操做失敗。會依據 a 對象 Mark Word 判斷是否指向當前線程的棧幀,若是是,說明當前線程已經擁有鎖了,直接進入代碼塊執行(可重入鎖)。
若是 a 對象的 Mark Word判斷是另一個線程擁有所,會升級鎖,鎖標誌位改成 (10)。
輕量級鎖解鎖,就是將 Lock Record
中的 a 的 mark word 拷貝,經過 CAS 替換 a 對象頭中的 mark word ,替換成功解鎖順利完成。
偏向鎖是比輕量級鎖更輕量的鎖。輕量級鎖,每次獲取鎖的時候,都會使用 CAS 判斷是否能夠加鎖,無論有沒有別的線程競爭。
偏向鎖呢,好比說 T 線程獲取到了 a 對象的偏向鎖,a 的 Mark Word 會記錄當前 T 線程的 id ,當下次獲取鎖的時候。T 線程再獲取 a 鎖的時候,只須要判斷 a 的 Mark Word 中的偏向鎖位和當前持有 a 鎖的線程 id,而再也不須要經過 CAS 操做獲取偏向鎖了。
延遲 6 秒建立 a 對象,這時已通過了偏向鎖延遲的時間,建立的對象爲可偏向對象。
public class JOLSample_13_BiasedLocking { public static void main(String[] args) throws Exception { TimeUnit.SECONDS.sleep(6); final A a = new A(); ClassLayout layout = ClassLayout.parseInstance(a); out.println("**** Fresh object"); out.println(layout.toPrintable()); synchronized (a) { out.println("**** With the lock"); out.println(layout.toPrintable()); } out.println("**** After the lock"); out.println(layout.toPrintable()); } public static class A { // no fields } }
<img src="http://oss.mflyyou.cn/blog/20200613231613.png?author=zhangpanqin" alt="image-20200613231613645" style="zoom: 33%;" />
寫了一個 demo ,驗證 偏向鎖,輕量級鎖,重量級鎖的逐漸升級過程。
public class JOLSample_14_FatLocking { public static void main(String[] args) throws Exception { // 延遲六秒執行例子,建立的 a 爲可偏向對象 TimeUnit.SECONDS.sleep(6); final A a = new A(); ClassLayout layout = ClassLayout.parseInstance(a); out.println("**** 查看初始化 a 的對象頭"); out.println(layout.toPrintable()); // 這裏模擬獲取鎖,當前獲取到的鎖爲 偏向鎖 Thread t = new Thread(() -> { synchronized (a) { } }); t.start(); // 阻塞等待獲取 t 線程完成 t.join(); out.println("**** t 線程得到鎖以後"); out.println(layout.toPrintable()); final Thread t2 = new Thread(() -> { synchronized (a) { // a 的存在兩個想成競爭鎖,鎖升級爲輕量級鎖 out.println("**** t2 第二次獲取鎖"); out.println(layout.toPrintable()); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } } }); // 開啓 t3 線程模擬競爭,t3 會自旋得到鎖,因爲 t2 阻塞了 3 秒,t3 自旋是得不到鎖的,鎖升級爲重量級鎖 final Thread t3 = new Thread(() -> { synchronized (a) { out.println("**** t3 不停獲取鎖"); out.println(layout.toPrintable()); } }); t2.start(); // 爲了 t2 先得到鎖,這裏阻塞 10ms ,再開啓 t3 線程 TimeUnit.MILLISECONDS.sleep(10); t3.start();t2.join();t3.join(); // 驗證 gc 可使鎖降級 System.gc(); out.println("**** After System.gc()"); out.println(layout.toPrintable()); } public static class A {} }
**** 查看初始化 a 的對象頭 com.fly.blog.sync.JOLSample_14_FatLocking$A object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5) **** t 線程得到鎖以後 com.fly.blog.sync.JOLSample_14_FatLocking$A object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 f0 52 d3 (00000101 11110000 01010010 11010011) (-749539323) **** t2 第二次獲取鎖 com.fly.blog.sync.JOLSample_14_FatLocking$A object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) f8 38 c3 10 (11111000 00111000 11000011 00010000) (281229560) **** t3 不停獲取鎖 com.fly.blog.sync.JOLSample_14_FatLocking$A object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 5a 1b 82 d2 (01011010 00011011 10000010 11010010) (-763225254) **** After System.gc() com.fly.blog.sync.JOLSample_14_FatLocking$A object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 09 00 00 00 (00001001 00000000 00000000 00000000) (9)
觀察各階段對象頭中的 偏向鎖位
和 鎖標誌位
。能夠看到鎖在不斷升級。而後看到 gc 以後,又變成了無鎖。
t2
線程持有鎖 a
的 輕量級鎖
的時候,t3 也在得到 a 的 輕量級鎖
,CAS
修改 a 的 Mark Word
爲 t3 全部失敗。致使了鎖升級爲重量級鎖,設置 a 的鎖標誌位爲 10,而且將 Mark Word
指針指向一個 monitor對象,並將當前線程阻塞,將當前線程放入到 _EntryList
隊列中。當 t2 執行完以後,它解鎖的時候發現當前鎖已經升級爲重量級鎖,釋放鎖的時候,會喚醒 _EntryList
的線程,讓它們去搶 a 鎖。
class ObjectMonitor() { _owner = NULL; // 持有這把鎖監視器線程 _WaitSet = NULL; // 處於wait狀態的線程,會被加入到_WaitSet _EntryList = NULL ; // 處於等待鎖block狀態的線程,會被加入到該列表 }
Java Language Specification https://docs.oracle.com/javase/specs/jls/se8/html/index.html Every object, in addition to having an associated monitor, has an associated wait set. A wait set is a set of threads. When an object is first created, its wait set is empty. Elementary actions that add threads to and remove threads from wait sets are atomic. Wait sets are manipulated solely through the methods Object.wait, Object.notify, and Object.notifyAll.
調用對象的 Object.wait
方法,該線程會釋放鎖,並將當前線程放入到 monitor 的 _WaitSet
隊列中,等某個線程調用 Object.notify, and Object.notifyAll
,實際就是喚醒 _WaitSet
中的線程。
本文由 張攀欽的博客 http://www.mflyyou.cn/ 創做。 可自由轉載、引用,但需署名做者且註明文章出處。如轉載至微信公衆號,請在文末添加做者公衆號二維碼。微信公衆號名稱:Mflyyou