Java?synchronized同步關鍵字工作原理
一、簡介
synchronized是一個同步關鍵字,在某些多線程場景下,如果不進行同步會導致共享數據不安全,synchronized關鍵字就可以用于代碼同步。
synchronized主要有3種使用形式:
- 修飾普通同步方法
鎖的對象是當前實例對象;
- 修飾靜態(tài)同步方法
鎖的對象是當前的類的Class字節(jié)碼對象;
- 修飾同步代碼塊
鎖的對象是synchronized后面括號里配置的對象,可以是某個對象,也可以是某個類的.class對象;
二、synchronized的特性
1)、原子性
原子性指的是在一次或多次操作中,要么所有的操作都執(zhí)行并且不會受其他因素干擾而中斷,要么所有的操作都不執(zhí)行。
2)、可見性
可見性是指一個線程對共享變量進行了修改,另一個線程可以立即讀取得到修改后的最新值。
獲取鎖時,會清空當前線程工作內存中共享變量的副本值,重新從主內存中獲取變量最新的值;
釋放鎖時,會將工作內存的值重新刷新回主內存;
3)、有序性
有序性是指程序中代碼的執(zhí)行順序,Java在編譯時和運行時會對代碼進行優(yōu)化,會導致程序最終的執(zhí)行順序不一定就是我們編寫代碼時的順序。
例如,instance = new Singleton()實例化對象的語句分為三步:
- 1、分配對象的內存空間;
- 2、初始化對象;
- 3、設置實例對象指向剛分配的內存地址;
上述第二步操作需要依賴第一步,但是第三步操作不需要依賴第二步,所以執(zhí)行順序可能為:1->2->3、1->3->2,當執(zhí)行順序為1->3->2時,可能實例對象還沒正確初始化,我們直接拿到使用的時候可能會報錯。
synchronized的有序性是依靠內存屏障實現的,在 monitorenter 指令和 Load 屏障之后,會加一個 Acquire屏障,這個屏障的作用是禁止同步代碼塊里面的讀操作和外面的讀寫操作之間發(fā)生指令重排,在 monitorexit 指令前加一個Release屏障,也是禁止同步代碼塊里面的寫操作和外面的讀寫操作之間發(fā)生重排序。如下:
int a = 0; synchronize (this){ //monitorenter // Load內存屏障 // Acquire屏障,禁止代碼塊內部的讀,和外面的讀寫發(fā)生指令重排 int b = a; a = 10; //注意:內部還是會發(fā)生指令重排 // Release屏障,禁止寫,和外面的讀寫發(fā)生指令重排 } //monitorexit //Store內存屏障
4)、可重入特性
可重入指的就是一個線程可以多次執(zhí)行synchronized,重復獲取同一把鎖。
舉個例子:
public class RenentrantDemo { // 鎖對象 private static Object obj = new Object(); public static void main(String[] args) { // 自定義Runnable對象 Runnable runnable = () -> { // 使用嵌套的同步代碼塊 synchronized (obj) { System.out.println(Thread.currentThread().getName() + "第一次獲取鎖資源..."); synchronized (obj) { System.out.println(Thread.currentThread().getName() + "第二次獲取鎖資源..."); synchronized (obj) { System.out.println(Thread.currentThread().getName() + "第三次獲取鎖資源..."); } } } }; new Thread(runnable, "t1").start(); } }
運行結果:
t1第一次獲取鎖資源...
t1第二次獲取鎖資源...
t1第三次獲取鎖資源...
三、synchonized的使用及通過反匯編分析其原理
修飾代碼塊
public class SynchronizedDemo01 { // 鎖對象 private static Object obj = new Object(); public static void main(String[] args) { synchronized (obj) { System.out.println("execute main()..."); } } }
使用javap -p -v .\SynchronizedDemo01.class命令對字節(jié)碼進行反匯編,查看字節(jié)碼指令:
monitorenter指令
官網對monitorenter指令的介紹,就是說每一個對象都會和一個監(jiān)視器對象monitor關聯,監(jiān)視器被占用時會被鎖住,其他線程無法來獲取該monitor。 當JVM執(zhí)行某個線程的某個方法內部的monitorenter時,它會嘗試去獲取當前對象對應的monitor的所有權。大體過程如下:
1. 若monior的進入數為0,線程可以進入monitor,并將monitor的進入數置為1,當前線程成為monitor的owner(擁有這把鎖的線程);
2. 若線程已擁有monitor的所有權,允許它重入monitor,則進入monitor的進入數加1(記錄線程擁有鎖的次數);
3. 若其他線程已經占有monitor的所有權,那么當前嘗試獲取monitor的所有權的線程會被阻塞,直到monitor的進入數變?yōu)?,才能重新嘗試獲取monitor的所有權;
monitorexit指令
官網對monitorexit指令的介紹,就是說能執(zhí)行monitorexit指令的線程一定是擁有當前對象的monitor的所有權的線程;執(zhí)行monitorexit時會將monitor的進入數減1,當monitor的進入數減為0時,當前線程退出。
為什么字節(jié)碼中存在兩個monitorexit指令?
其實第二個monitorexit指令,是在程序發(fā)生異常時候用到的,也就說明了synchronized在發(fā)生異常時,會自動釋放鎖。
ObjectMonitor對象監(jiān)視器結構如下:
ObjectMonitor() { _header = NULL; //鎖對象的原始對象頭 _count = 0; //搶占當前鎖的線程數量 _waiters = 0, //調用wait方法后等待的線程數量 _recursions = 0; //記錄鎖重入次數 _object = NULL; _owner = NULL; //指向持有ObjectMonitor的線程 _WaitSet = NULL; //處于wait狀態(tài)的線程隊列,等待被喚醒 _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; //等待鎖的線程隊列 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; _previous_owner_tid = 0; }
修飾普通方法
public class SynchronizedDemo02 { public static void main(String[] args) { } // 修飾普通方法 public synchronized void add() { System.out.println("add..."); } }
使用javap -p -v .\SynchronizedDemo02.class命令對字節(jié)碼進行反匯編,查看字節(jié)碼指令:
如上圖,我們可以看到同步方法在反匯編后,不再是通過插入monitorentry和monitorexit指令實現,而是會增加 ACC_SYNCHRONIZED 標識隱式實現的,如果方法表結構(method_info Structure)中的ACC_SYNCHRONIZED標志被設置,那么線程在執(zhí)行方法前會先去獲取對象的monitor對象,如果獲取成功則執(zhí)行方法代碼,執(zhí)行完畢后釋放monitor對象,如果monitor對象已經被其它線程獲取,那么當前線程被阻塞。
修飾靜態(tài)方法
public class SynchronizedDemo03 { public static void main(String[] args) { add(); } // 修飾靜態(tài)方法 public synchronized static void add() { System.out.println("add..."); } }
使用javap -p -v .\SynchronizedDemo03.class命令對字節(jié)碼進行反匯編,查看字節(jié)碼指令:
四、synchronized鎖對象存在哪里
之前對對象的內存布局的介紹中,我們知道一個對象,包括對象頭、實例數據、對齊填充。而對象頭又包括mark word標記字、類型指針、數組長度(只有數組對象才有)。在mark word標記字中,有一塊區(qū)域主要存放關于鎖的信息。
存在鎖對象的對象頭的MarkWord標記字中。如下圖:
五、synchronized與lock的區(qū)別
區(qū)別 | synchronized | lock |
1 | 關鍵字 | 接口 |
2 | 自動釋放鎖 | 必須手動調用unlock()方法釋放鎖 |
3 | 不能知道線程是否拿到鎖 | 可以知道線程是否拿到鎖 |
4 | 能鎖住方法和代碼塊 | 只能鎖住代碼塊 |
5 | 讀、寫操作都阻塞 | 可以使用讀鎖,提高多線程讀效率; |
6 | 非公平鎖 | 通過構造方法可指定是公平鎖/非公平鎖 |
六、總結
1、synchronized修飾代碼塊的時候,通過在生成的字節(jié)碼指令中插入monitorenter和monitorexit指令來完成對對象監(jiān)視器鎖的獲取和釋放;
2、synchronized修飾普通方法和靜態(tài)方法的時候,通過在字節(jié)碼中的方法頭信息中添ACC_SYNCHRONIZED標識,線程在執(zhí)行方法前會先去獲取對象的monitor對象,如果獲取成功則執(zhí)行方法代碼,執(zhí)行完畢后釋放monitor對象;
3、synchronized修飾代碼塊,鎖的對象就是代碼塊中的對象;修飾普通方法的時候,鎖的對象就是當前對象this;修飾靜態(tài)方法的時候,鎖的對象就是當前類的Class字節(jié)碼對象(類對象);
4、使用synchronized修飾實例對象時,如果一個線程正在訪問實例對象的一個synchronized方法時,其它線程不僅不能訪問該synchronized方法,該對象的其它synchronized方法也不能訪問,因為一個對象只有一個監(jiān)視器鎖對象,但是其它線程可以訪問該對象的非synchronized方法。
5、線程A訪問實例對象的非static synchronized方法時,線程B也可以同時訪問實例對象的static synchronized方法,因為前者獲取的是實例對象的監(jiān)視器鎖,而后者獲取的是類對象的監(jiān)視器鎖,兩者不存在互斥關系。
到此這篇關于Java synchronized同步關鍵字工作原理的文章就介紹到這了,更多相關Java synchronized內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
- Java同步鎖Synchronized底層源碼和原理剖析(推薦)
- java同步鎖的正確使用方法(必看篇)
- 95%的Java程序員人都用不好Synchronized詳解
- Java synchronized偏向鎖的概念與使用
- Java?synchronized輕量級鎖實現過程淺析
- Java synchronized重量級鎖實現過程淺析
- Java @Transactional與synchronized使用的問題
- Java?synchronized與死鎖深入探究
- Java synchronized與CAS使用方式詳解
- 淺析Java關鍵詞synchronized的使用
- synchronized及JUC顯式locks?使用原理解析
- java鎖synchronized面試常問總結
- Java?HashTable與Collections.synchronizedMap源碼深入解析
- Java?Synchronized鎖的使用詳解
- AQS加鎖機制Synchronized相似點詳解
- Java必會的Synchronized底層原理剖析
- 一個例子帶你看懂Java中synchronized關鍵字到底怎么用
- 詳解Java?Synchronized的實現原理
- Synchronized?和?ReentrantLock?的實現原理及區(qū)別
- Java同步鎖synchronized用法的最全總結
相關文章
spring batch使用reader讀數據的內存容量問題詳解
這篇文章主要介紹了spring batch使用reader讀數據的內存容量問題詳解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-07-07