死磕 java同步系列之synchronized解析
問題
(1)synchronized的特性?
(2)synchronized的實(shí)現(xiàn)原理?
(3)synchronized是否可重入?
(4)synchronized是否是公平鎖?
(5)synchronized的優(yōu)化?
(6)synchronized的五種使用方式?
簡介
synchronized關(guān)鍵字是Java里面最基本的同步手段,它經(jīng)過編譯之后,會在同步塊的前后分別生成 monitorenter 和 monitorexit 字節(jié)碼指令,這兩個字節(jié)碼指令都需要一個引用類型的參數(shù)來指明要鎖定和解鎖的對象。
實(shí)現(xiàn)原理
在學(xué)習(xí)Java內(nèi)存模型的時候,我們介紹過兩個指令:lock 和 unlock。
lock,鎖定,作用于主內(nèi)存的變量,它把主內(nèi)存中的變量標(biāo)識為一條線程獨(dú)占狀態(tài)。
unlock,解鎖,作用于主內(nèi)存的變量,它把鎖定的變量釋放出來,釋放出來的變量才可以被其它線程鎖定。
但是這兩個指令并沒有直接提供給用戶使用,而是提供了兩個更高層次的指令 monitorenter 和 monitorexit 來隱式地使用 lock 和 unlock 指令。
而 synchronized 就是使用 monitorenter 和 monitorexit 這兩個指令來實(shí)現(xiàn)的。
根據(jù)JVM規(guī)范的要求,在執(zhí)行monitorenter指令的時候,首先要去嘗試獲取對象的鎖,如果這個對象沒有被鎖定,或者當(dāng)前線程已經(jīng)擁有了這個對象的鎖,就把鎖的計數(shù)器加1,相應(yīng)地,在執(zhí)行monitorexit的時候會把計數(shù)器減1,當(dāng)計數(shù)器減小為0時,鎖就釋放了。
我們還是來上一段代碼,看看編譯后的字節(jié)碼長啥樣來學(xué)習(xí):
public class SynchronizedTest{ public static void sync(){ synchronized(SynchronizedTest.class){ synchronized(SynchronizedTest.class){ } } } public static void main(String[] args){ } }
我們這段代碼很簡單,只是簡單地對SynchronizedTest.class對象加了兩次synchronized,除此之外,啥也沒干。
編譯后的sync()方法的字節(jié)碼指令如下,為了便于閱讀,彤哥特意加上了注釋:
// 加載常量池中的SynchronizedTest類對象到操作數(shù)棧中 0 ldc #2 <com/coolcoding/code/synchronize/SynchronizedTest> // 復(fù)制棧頂元素 2 dup// 存儲一個引用到本地變量0中,后面的0表示第幾個變量 3 astore_0 // 調(diào)用monitorenter,它的參數(shù)變量0,也就是上面的SynchronizedTest類對象 4 monitorenter // 再次加載常量池中的SynchronizedTest類對象到操作數(shù)棧中 5 ldc #2 <com/coolcoding/code/synchronize/SynchronizedTest> // 復(fù)制棧頂元素 7 dup // 存儲一個引用到本地變量1中 8 astore_1 // 再次調(diào)用monitorenter,它的參數(shù)是變量1,也還是SynchronizedTest類對象 9 monitorenter // 從本地變量表中加載第1個變量 10 aload_1 // 調(diào)用monitorexit解鎖,它的參數(shù)是上面加載的變量1 11 monitorexit // 跳到第20行 12 goto 20 (+8) 15 astore_2 16 aload_1 17 monitorexit 18 aload_2 19 athrow // 從本地變量表中加載第0個變量 20 aload_0 // 調(diào)用monitorexit解鎖,它的參數(shù)是上面加載的變量0 21 monitorexit // 跳到第30行 22 goto 30 (+8) 25 astore_326 aload_0 27 monitorexit28 aload_329 athrow // 方法返回,結(jié)束 30 return
按照彤哥的注釋讀起來,字節(jié)碼比較簡單,我們的synchronized鎖定的是SynchronizedTest類對象,可以看到它從常量池中加載了兩次SynchronizedTest類對象,分別存儲在本地變量0和本地變量1中,解鎖的時候正好是相反的順序,先解鎖變量1,再解鎖變量0,實(shí)際上變量0和變量1指向的是同一個對象,所以synchronized是可重入的。
至于,被加鎖的對象具體在對象頭中是怎么存儲的,彤哥這里就不細(xì)講了,有興趣的可以看看《Java并發(fā)編程的藝術(shù)》這本書。
原子性、可見性、有序性
前面講解Java內(nèi)存模型的時候我們說過內(nèi)存模型主要就是用來解決緩存一致性的問題的,而緩存一致性主要包括原子性、可見性、有序性。
那么,synchronized關(guān)鍵字能否保證這三個特性呢?
還是回到Java內(nèi)存模型上來,synchronized關(guān)鍵字底層是通過monitorenter和monitorexit實(shí)現(xiàn)的,而這兩個指令又是通過lock和unlock來實(shí)現(xiàn)的。
而lock和unlock在Java內(nèi)存模型中是必須滿足下面四條規(guī)則的:
(1)一個變量同一時刻只允許一條線程對其進(jìn)行l(wèi)ock操作,但lock操作可以被同一個線程執(zhí)行多次,多次執(zhí)行l(wèi)ock后,只有執(zhí)行相同次數(shù)的unlock操作,變量才能被解鎖。
(2)如果對一個變量執(zhí)行l(wèi)ock操作,將會清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個變量前,需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值;
(3)如果一個變量沒有被lock操作鎖定,則不允許對其執(zhí)行unlock操作,也不允許unlock一個其它線程鎖定的變量;
(4)對一個變量執(zhí)行unlock操作之前,必須先把此變量同步回主內(nèi)存中,即執(zhí)行store和write操作;
通過規(guī)則(1),我們知道對于lock和unlock之間的代碼,同一時刻只允許一個線程訪問,所以,synchronized是具有原子性的。
通過規(guī)則(1)(2)和(4),我們知道每次lock和unlock時都會從主內(nèi)存加載變量或把變量刷新回主內(nèi)存,而lock和unlock之間的變量(這里是指鎖定的變量)是不會被其它線程修改的,所以,synchronized是具有可見性的。
通過規(guī)則(1)和(3),我們知道所有對變量的加鎖都要排隊進(jìn)行,且其它線程不允許解鎖當(dāng)前線程鎖定的對象,所以,synchronized是具有有序性的。
綜上所述,synchronized是可以保證原子性、可見性和有序性的。
公平鎖 VS 非公平鎖
通過上面的學(xué)習(xí),我們知道了synchronized的實(shí)現(xiàn)原理,并且它是可重入的,那么,它是否是公平鎖呢?
直接上菜:
public class SynchronizedTest { public static void sync(String tips) { synchronized (SynchronizedTest.class) { System.out.println(tips); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) throws InterruptedException { new Thread(()->sync("線程1")).start(); Thread.sleep(100); new Thread(()->sync("線程2")).start(); Thread.sleep(100); new Thread(()->sync("線程3")).start(); Thread.sleep(100); new Thread(()->sync("線程4")).start(); } }
在這段程序中,我們起了四個線程,且分別間隔100ms啟動,每個線程里面打印一句話后等待1000ms,如果synchronized是公平鎖,那么打印的結(jié)果應(yīng)該依次是 線程1、2、3、4。
但是,實(shí)際運(yùn)行的結(jié)果幾乎不會出現(xiàn)上面的樣子,所以,synchronized是一個非公平鎖。
鎖優(yōu)化
Java在不斷進(jìn)化,同樣地,Java中像synchronized這種古老的東西也在不斷進(jìn)化,比如ConcurrentHashMap在jdk7的時候還是使用ReentrantLock加鎖的,在jdk8的時候已經(jīng)換成了原生的synchronized了,可見synchronized有原生的支持,它的進(jìn)化空間還是很大的。
那么,synchronized有哪些進(jìn)化中的狀態(tài)呢?
我們這里稍做一些簡單地介紹:
(1)偏向鎖,是指一段同步代碼一直被一個線程訪問,那么這個線程會自動獲取鎖,降低獲取鎖的代價。
(2)輕量級鎖,是指當(dāng)鎖是偏向鎖時,被另一個線程所訪問,偏向鎖會升級為輕量級鎖,這個線程會通過自旋的方式嘗試獲取鎖,不會阻塞,提高性能。
(3)重量級鎖,是指當(dāng)鎖是輕量級鎖時,當(dāng)自旋的線程自旋了一定的次數(shù)后,還沒有獲取到鎖,就會進(jìn)入阻塞狀態(tài),該鎖升級為重量級鎖,重量級鎖會使其他線程阻塞,性能降低。
總結(jié)
(1)synchronized在編譯時會在同步塊前后生成monitorenter和monitorexit字節(jié)碼指令;
(2)monitorenter和monitorexit字節(jié)碼指令需要一個引用類型的參數(shù),基本類型不可以哦;
(3)monitorenter和monitorexit字節(jié)碼指令更底層是使用Java內(nèi)存模型的lock和unlock指令;
(4)synchronized是可重入鎖;
(5)synchronized是非公平鎖;
(6)synchronized可以同時保證原子性、可見性、有序性;
(7)synchronized有三種狀態(tài):偏向鎖、輕量級鎖、重量級鎖;
彩蛋——synchronized的五種使用方式
通過上面的分析,我們知道synchronized是需要一個引用類型的參數(shù)的,而這個引用類型的參數(shù)在Java中其實(shí)可以分成三大類:類對象、實(shí)例對象、普通引用,使用方式分別如下:
public class SynchronizedTest2 { public static final Object lock = new Object(); // 鎖的是SynchronizedTest.class對象 public static synchronized void sync1() { } public static void sync2() { // 鎖的是SynchronizedTest.class對象 synchronized (SynchronizedTest.class) { } } // 鎖的是當(dāng)前實(shí)例this public synchronized void sync3() { } public void sync4() { // 鎖的是當(dāng)前實(shí)例this synchronized (this){ } } public void sync5() { // 鎖的是指定對象lock synchronized (lock) { } } }
總結(jié)
在方法上使用synchronized的時候要注意,會隱式傳參,分為靜態(tài)方法和非靜態(tài)方法,靜態(tài)方法上的隱式參數(shù)為當(dāng)前類對象,非靜態(tài)方法上的隱式參數(shù)為當(dāng)前實(shí)例this。
另外,多個synchronized只有鎖的是同一個對象,它們之間的代碼才是同步的,這一點(diǎn)在使用synchronized的時候一定要注意。
這篇文章就到這里,希望可以給你帶來幫助,也希望您可以多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
Java設(shè)計模式之組合模式(Composite模式)介紹
這篇文章主要介紹了Java設(shè)計模式之組合模式(Composite模式)介紹,Composite定義:將對象以樹形結(jié)構(gòu)組織起來,以達(dá)成“部分-整體” 的層次結(jié)構(gòu),使得客戶端對單個對象和組合對象的使用具有一致性,需要的朋友可以參考下2015-03-03springboot讀取nacos配置文件的實(shí)現(xiàn)
SpringBoot注冊服務(wù)到Nacos上,由Nacos來做服務(wù)的管理,本文主要介紹了springboot讀取nacos配置文件的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-05-05詳解spring-boot actuator(監(jiān)控)配置和使用
本篇文章主要介紹了spring-boot actuator(監(jiān)控)配置和使用,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-09-09SpringBoot中發(fā)送QQ郵件功能的實(shí)現(xiàn)代碼
這篇文章主要介紹了SpringBoot中發(fā)送QQ郵件功能的實(shí)現(xiàn)代碼,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2018-02-02java使用Dijkstra算法實(shí)現(xiàn)單源最短路徑
這篇文章主要為大家詳細(xì)介紹了java使用Dijkstra算法實(shí)現(xiàn)單源最短路徑,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-01-01java獲取登錄者IP和登錄時間的兩種實(shí)現(xiàn)代碼詳解
這篇文章主要介紹了java獲取登錄者IP和登錄時間的實(shí)現(xiàn)代碼,本文通過兩種結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-07-07