1202年最新最详细最全的synchronized知识详解
synchronized詳解
前言
通俗:造成線程安全問(wèn)題的主要誘因有兩點(diǎn):
- 存在共享數(shù)據(jù)(也稱臨界資源)
- 存在多條線程共同操作共享數(shù)據(jù)
學(xué)術(shù):造成線程安全問(wèn)題的主要誘因有兩點(diǎn):
- 主內(nèi)存和線程的工作內(nèi)存而導(dǎo)致的內(nèi)存可見(jiàn)性問(wèn)題,
- 重排序?qū)е碌膯?wèn)題,需要知道happens-before規(guī)則。
當(dāng)存在多個(gè)線程操作共享數(shù)據(jù)時(shí),需要保證同一時(shí)刻有且只有一個(gè)線程在操作共享數(shù)據(jù),其他線程必須等到該線程處理完數(shù)據(jù)后再進(jìn)行,這種方式的名稱叫·互斥鎖,也就是說(shuō)當(dāng)一個(gè)共享數(shù)據(jù)被當(dāng)前正在訪問(wèn)的線程加上互斥鎖后,在同一個(gè)時(shí)刻,其他線程只能處于等待的狀態(tài),直到當(dāng)前線程處理完畢釋放該鎖。
關(guān)鍵字 synchronized可以保證(1)在同一個(gè)時(shí)刻,只有一個(gè)線程可以執(zhí)行某個(gè)方法或者某個(gè)代碼塊(主要是對(duì)方法或者代碼塊中存在共享數(shù)據(jù)的操作),(2)synchronized保證一個(gè)線程的變化(主要是共享數(shù)據(jù)的變化)被其他線程所看到(保證可見(jiàn)性,完全可以替代Volatile功能)也就是happens-before規(guī)則。
標(biāo)注:在學(xué)習(xí)中需要修改的內(nèi)容以及筆記全在這里 www.javanode.cn,謝謝!有任何不妥的地方望糾正
synchronized主要方式
synchronized關(guān)鍵字最主要有以下3種應(yīng)用方式,下面分別介紹
- 修飾實(shí)例方法,作用于當(dāng)前實(shí)例加鎖,進(jìn)入同步代碼前要獲得當(dāng)前實(shí)例的鎖
- 修飾靜態(tài)方法,作用于當(dāng)前類對(duì)象加鎖,進(jìn)入同步代碼前要獲得當(dāng)前類對(duì)象的鎖
- 修飾代碼塊,指定加鎖對(duì)象,對(duì)給定對(duì)象加鎖,進(jìn)入同步代碼庫(kù)前要獲得給定對(duì)象的鎖。
synchronized使用
作用于實(shí)例方法
所謂的實(shí)例對(duì)象鎖就是用synchronized修飾實(shí)例對(duì)象中的實(shí)例方法,注意是實(shí)例方法不包括靜態(tài)方法
public class AccountingSync implements Runnable{//共享資源(臨界資源)static int i=0;/*** synchronized 修飾實(shí)例方法*/public synchronized void increase(){i++;}@Overridepublic void run() {for(int j=0;j<1000000;j++){increase();}}public static void main(String[] args) throws InterruptedException {AccountingSync instance=new AccountingSync();Thread t1=new Thread(instance);Thread t2=new Thread(instance);t1.start();t2.start();t1.join();t2.join();System.out.println(i);}/*** 輸出結(jié)果:* 2000000*/ }我們應(yīng)該注意到synchronized修飾的是實(shí)例方法increase,在這樣的情況下,當(dāng)前線程的鎖便是實(shí)例對(duì)象instance,注意Java中的線程同步鎖可以是任意對(duì)象。
這里我們還需要意識(shí)到,當(dāng)一個(gè)線程正在訪問(wèn)一個(gè)對(duì)象的 synchronized 實(shí)例方法,那么其他線程不能訪問(wèn)該對(duì)象的其他 synchronized 方法,畢竟一個(gè)對(duì)象只有一把鎖,當(dāng)一個(gè)線程獲取了該對(duì)象的鎖之后,其他線程無(wú)法獲取該對(duì)象的鎖,所以無(wú)法訪問(wèn)該對(duì)象的其他synchronized實(shí)例方法,但是其他線程還是可以訪問(wèn)該實(shí)例對(duì)象的其他非synchronized方法,當(dāng)然如果是一個(gè)線程 A 需要訪問(wèn)實(shí)例對(duì)象 obj1 的 synchronized 方法 f1(當(dāng)前對(duì)象鎖是obj1),另一個(gè)線程 B 需要訪問(wèn)實(shí)例對(duì)象 obj2 的 synchronized 方法 f2(當(dāng)前對(duì)象鎖是obj2),這樣是允許的,因?yàn)閮蓚€(gè)實(shí)例對(duì)象鎖并不同相同,此時(shí)如果兩個(gè)線程操作數(shù)據(jù)并非共享的,線程安全是有保障的,
遺憾的是如果兩個(gè)線程操作的是共享數(shù)據(jù),那么線程安全就有可能無(wú)法保證了,如下代碼將演示出該現(xiàn)象
public class AccountingSyncBad implements Runnable{static int i=0;public synchronized void increase(){i++;}@Overridepublic void run() {for(int j=0;j<1000000;j++){increase();}}public static void main(String[] args) throws InterruptedException {//new新實(shí)例Thread t1=new Thread(new AccountingSyncBad());//new新實(shí)例Thread t2=new Thread(new AccountingSyncBad());t1.start();t2.start();//join含義:當(dāng)前線程A等待thread線程終止之后才能從thread.join()返回t1.join();t2.join();System.out.println(i);} }上述代碼與前面不同的是我們同時(shí)創(chuàng)建了兩個(gè)新實(shí)例AccountingSyncBad,然后啟動(dòng)兩個(gè)不同的線程對(duì)共享變量i進(jìn)行操作,但很遺憾操作結(jié)果是1452317而不是期望結(jié)果2000000,因?yàn)樯鲜龃a犯了嚴(yán)重的錯(cuò)誤,雖然我們使用synchronized修飾了increase方法,但卻new了兩個(gè)不同的實(shí)例對(duì)象,這也就意味著存在著兩個(gè)不同的實(shí)例對(duì)象鎖,因此t1和t2都會(huì)進(jìn)入各自的對(duì)象鎖,也就是說(shuō)t1和t2線程使用的是不同的鎖,因此線程安全是無(wú)法保證的。解決這種困境的的方式是將synchronized作用于靜態(tài)的increase方法,這樣的話,對(duì)象鎖就當(dāng)前類對(duì)象,由于無(wú)論創(chuàng)建多少個(gè)實(shí)例對(duì)象,但對(duì)于的類對(duì)象擁有只有一個(gè),所有在這樣的情況下對(duì)象鎖就是唯一的。下面我們看看如何使用將synchronized作用于靜態(tài)的increase方法。
作用于靜態(tài)方法
當(dāng)synchronized作用于靜態(tài)方法時(shí),其鎖就是當(dāng)前類的class對(duì)象鎖。由于靜態(tài)成員不專屬于任何一個(gè)實(shí)例對(duì)象,是類成員,因此通過(guò)class對(duì)象鎖可以控制靜態(tài) 成員的并發(fā)操作。需要注意的是如果一個(gè)線程A調(diào)用一個(gè)實(shí)例對(duì)象的非static synchronized方法,而線程B需要調(diào)用這個(gè)實(shí)例對(duì)象所屬類的靜態(tài) synchronized方法,是允許的,不會(huì)發(fā)生互斥現(xiàn)象,因?yàn)樵L問(wèn)靜態(tài) synchronized 方法占用的鎖是當(dāng)前類的class對(duì)象,而訪問(wèn)非靜態(tài) synchronized 方法占用的鎖是當(dāng)前實(shí)例對(duì)象鎖,看如下代碼
public class AccountingSyncClass implements Runnable{static int i=0;/*** 作用于靜態(tài)方法,鎖是當(dāng)前class對(duì)象,也就是* AccountingSyncClass類對(duì)應(yīng)的class對(duì)象*/public static synchronized void increase(){i++;}/*** 非靜態(tài),訪問(wèn)時(shí)鎖不一樣不會(huì)發(fā)生互斥*/public synchronized void increase4Obj(){i++;}@Overridepublic void run() {for(int j=0;j<1000000;j++){increase();}}public static void main(String[] args) throws InterruptedException {//new新實(shí)例Thread t1=new Thread(new AccountingSyncClass());//new心事了Thread t2=new Thread(new AccountingSyncClass());//啟動(dòng)線程t1.start();t2.start();t1.join();t2.join();System.out.println(i);} }由于synchronized關(guān)鍵字修飾的是靜態(tài)increase方法,與修飾實(shí)例方法不同的是,其鎖對(duì)象是當(dāng)前類的class對(duì)象。注意代碼中的increase4Obj方法是實(shí)例方法,其對(duì)象鎖是當(dāng)前實(shí)例對(duì)象,如果別的線程調(diào)用該方法,將不會(huì)產(chǎn)生互斥現(xiàn)象,畢竟鎖對(duì)象不同,但我們應(yīng)該意識(shí)到這種情況下可能會(huì)發(fā)現(xiàn)線程安全問(wèn)題(操作了共享靜態(tài)變量i)。
作用于同步代碼塊
除了使用關(guān)鍵字修飾實(shí)例方法和靜態(tài)方法外,還可以使用同步代碼塊,在某些情況下,我們編寫的方法體可能比較大,同時(shí)存在一些比較耗時(shí)的操作,而需要同步的代碼又只有一小部分,如果直接對(duì)整個(gè)方法進(jìn)行同步操作,可能會(huì)得不償失,此時(shí)我們可以使用同步代碼塊的方式對(duì)需要同步的代碼進(jìn)行包裹,這樣就無(wú)需對(duì)整個(gè)方法進(jìn)行同步操作了,同步代碼塊的使用示例如下:
public class AccountingSync implements Runnable{static AccountingSync instance=new AccountingSync();static int i=0;@Overridepublic void run() {//省略其他耗時(shí)操作....//使用同步代碼塊對(duì)變量i進(jìn)行同步操作,鎖對(duì)象為instancesynchronized(instance){for(int j=0;j<1000000;j++){i++;}}}public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(instance);Thread t2=new Thread(instance);t1.start();t2.start();t1.join();t2.join();System.out.println(i);} }從代碼看出,將synchronized作用于一個(gè)給定的實(shí)例對(duì)象instance,即當(dāng)前實(shí)例對(duì)象就是鎖對(duì)象,每次當(dāng)線程進(jìn)入synchronized包裹的代碼塊時(shí)就會(huì)要求當(dāng)前線程持有instance實(shí)例對(duì)象鎖,如果當(dāng)前有其他線程正持有該對(duì)象鎖,那么新到的線程就必須等待,這樣也就保證了每次只有一個(gè)線程執(zhí)行i++;操作。當(dāng)然除了instance作為對(duì)象外。
我們還可以使用this對(duì)象(代表當(dāng)前實(shí)例)或者當(dāng)前類的class對(duì)象作為鎖,如下代碼:
//this,當(dāng)前實(shí)例對(duì)象鎖 synchronized(this){for(int j=0;j<1000000;j++){i++;} }//class對(duì)象鎖 synchronized(AccountingSync.class){for(int j=0;j<1000000;j++){i++;} }總結(jié)
synchronized鎖對(duì)象和鎖類是本質(zhì)都是對(duì)對(duì)象來(lái)加鎖。類也是一個(gè)特殊的對(duì)象。只不過(guò)類對(duì)象只有一個(gè)。對(duì)象內(nèi)鎖不同的屬性,兩個(gè)同步方法可以同時(shí)訪問(wèn)
synchronized底層語(yǔ)義原理
在JVM中,對(duì)象在內(nèi)存中的布局分為三塊區(qū)域:對(duì)象頭、實(shí)例數(shù)據(jù)和對(duì)齊填充。如下:
對(duì)象頭由mark word ,指向?qū)ο髮?shí)例數(shù)據(jù)的指針(Class Metadata Address),length組成,其結(jié)構(gòu)說(shuō)明如下表:
| 32/64bit | Mark Word | 存儲(chǔ)對(duì)象的hashCode、鎖信息或分代年齡或GC標(biāo)志等信息 |
| 32/64bit | Class Metadata Address | 類型指針指向?qū)ο蟮念愒獢?shù)據(jù),JVM通過(guò)這個(gè)指針確定該對(duì)象是哪個(gè)類的實(shí)例。 |
| 32/64bit | length | 當(dāng)對(duì)象是數(shù)組時(shí),length保存數(shù)組的長(zhǎng)度 |
其中Mark Word在默認(rèn)情況下存儲(chǔ)著對(duì)象的HashCode、分代年齡、鎖標(biāo)記位等以下是32位JVM的Mark Word默認(rèn)存儲(chǔ)結(jié)構(gòu)
| 無(wú)鎖狀態(tài) | 對(duì)象HashCode | 對(duì)象分代年齡 | 0 | 01 |
由于對(duì)象頭的信息是與對(duì)象自身定義的數(shù)據(jù)沒(méi)有關(guān)系的額外存儲(chǔ)成本,因此考慮到JVM的空間效率,Mark Word 被設(shè)計(jì)成為一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu),以便存儲(chǔ)更多有效的數(shù)據(jù),它會(huì)根據(jù)對(duì)象本身的狀態(tài)復(fù)用自己的存儲(chǔ)空間,
synchronized代碼塊底層原理
Java 虛擬機(jī)中的同步(Synchronization)基于進(jìn)入和退出管程(Monitor)對(duì)象實(shí)現(xiàn), 無(wú)論是顯式同步(有明確的 monitorenter 和 monitorexit 指令,即同步代碼塊)還是隱式同步(方法級(jí)的同步)都是如此。在 Java 語(yǔ)言中,同步用的最多的地方可能是被 synchronized 修飾的同步方法。同步方法 并不是由 monitorenter 和 monitorexit 指令來(lái)實(shí)現(xiàn)同步的,而是由方法調(diào)用指令讀取運(yùn)行時(shí)常量池中方法的 ACC_SYNCHRONIZED 標(biāo)志來(lái)隱式實(shí)現(xiàn)的,
深入JVM看字節(jié)碼,創(chuàng)建如下的代碼:
public class SynchronizedDemo2 {Object object = new Object();public void method1() {synchronized (object) {}} }從字節(jié)碼中可知同步語(yǔ)句塊的實(shí)現(xiàn)使用的是monitorenter 和 monitorexit 指令。這也是添Synchronized關(guān)鍵字之后獨(dú)有的。執(zhí)行同步代碼塊首先要先執(zhí)行monitorenter指令,退出的時(shí)候是monitorexit指令。使用Synchronized進(jìn)行同步,其關(guān)鍵就是必須要對(duì)對(duì)象的監(jiān)視器monitor進(jìn)行獲取,當(dāng)執(zhí)行monitorenter指令時(shí),當(dāng)前線程將試圖獲取 objectref(即對(duì)象鎖) 所對(duì)應(yīng)的 monitor 的持有權(quán),當(dāng) objectref 的 monitor 的進(jìn)入計(jì)數(shù)器為 0,那線程可以成功取得 monitor,并將計(jì)數(shù)器值設(shè)置為 1,取鎖成功。Synchronized先天具有重入性。如果當(dāng)前線程已經(jīng)擁有 objectref 的 monitor 的持有權(quán),那它可以重入這個(gè) monitor (關(guān)于重入性稍后會(huì)分析),重入時(shí)計(jì)數(shù)器的值也會(huì)加 1。倘若其他線程已經(jīng)擁有 objectref 的 monitor 的所有權(quán),那當(dāng)前線程將被阻塞,直到正在執(zhí)行線程執(zhí)行完畢,即monitorexit指令被執(zhí)行,執(zhí)行線程將釋放 monitor(鎖)并設(shè)置計(jì)數(shù)器值為0 ,其他線程將有機(jī)會(huì)持有 monitor 。值得注意的是編譯器將會(huì)確保無(wú)論方法通過(guò)何種方式完成,方法中調(diào)用過(guò)的每條 monitorenter 指令都有執(zhí)行其對(duì)應(yīng) monitorexit 指令,而無(wú)論這個(gè)方法是正常結(jié)束還是異常結(jié)束。為了保證在方法異常完成時(shí) monitorenter 和 monitorexit 指令依然可以正確配對(duì)執(zhí)行,編譯器會(huì)自動(dòng)產(chǎn)生一個(gè)異常處理器,這個(gè)異常處理器聲明可處理所有的異常,它的目的就是用來(lái)執(zhí)行 monitorexit 指令。從字節(jié)碼中也可以看出多了一個(gè)monitorexit指令,它就是異常結(jié)束時(shí)被執(zhí)行的釋放monitor 的指令。
synchronized方法底層原理
方法級(jí)的同步是隱式,即無(wú)需通過(guò)字節(jié)碼指令來(lái)控制的,它實(shí)現(xiàn)在方法調(diào)用和返回操作之中。JVM可以從方法常量池中的方法表結(jié)構(gòu)(method_info Structure) 中的 ACC_SYNCHRONIZED 訪問(wèn)標(biāo)志區(qū)分一個(gè)方法是否同步方法。當(dāng)方法調(diào)用時(shí),調(diào)用指令將會(huì) 檢查方法的 ACC_SYNCHRONIZED 訪問(wèn)標(biāo)志是否被設(shè)置,如果設(shè)置了,執(zhí)行線程將先持有monitor(虛擬機(jī)規(guī)范中用的是管程一詞), 然后再執(zhí)行方法,最后再方法完成(無(wú)論是正常完成還是非正常完成)時(shí)釋放monitor。在方法執(zhí)行期間,執(zhí)行線程持有了monitor,其他任何線程都無(wú)法再獲得同一個(gè)monitor。如果一個(gè)同步方法執(zhí)行期間拋出了異常,并且在方法內(nèi)部無(wú)法處理此異常,那這個(gè)同步方法所持有的monitor將在異常拋到同步方法之外時(shí)自動(dòng)釋放。
//方法級(jí)的同步是隱式,public class SyncMethod {public int i;public synchronized void syncTask(){i++;}} ## 反編譯以后的字節(jié)碼public synchronized void syncTask();descriptor: ()Vflags: ACC_PUBLIC, ACC_SYNCHRONIZEDCode:stack=3, locals=1, args_size=10: aload_01: dup2: getfield #2 // Field i:I5: iconst_16: iadd7: putfield #2 // Field i:I10: returnLineNumberTable:line 9: 0line 10: 10LocalVariableTable:Start Length Slot Name Signature0 11 0 this Lcn/javanode/concurrent/key/synchronizedDesc/SyncMethod; }從字節(jié)碼中可以看出,synchronized修飾的方法并沒(méi)有monitorenter指令和monitorexit指令,取得代之的確實(shí)是ACC_SYNCHRONIZED標(biāo)識(shí),該標(biāo)識(shí)指明了該方法是一個(gè)同步方法,**JVM通過(guò)該ACC_SYNCHRONIZED訪問(wèn)標(biāo)志來(lái)辨別一個(gè)方法是否聲明為同步方法,從而執(zhí)行相應(yīng)的同步調(diào)用。**這便是synchronized鎖在同步代碼塊和同步方法上實(shí)現(xiàn)的基本原理。同時(shí)我們還必須注意到的是在Java早期版本中,synchronized屬于重量級(jí)鎖,效率低下,因?yàn)楸O(jiān)視器鎖(monitor)是依賴于底層的操作系統(tǒng)的Mutex Lock來(lái)實(shí)現(xiàn)的,而操作系統(tǒng)實(shí)現(xiàn)線程之間的切換時(shí)需要從用戶態(tài)轉(zhuǎn)換到核心態(tài),這個(gè)狀態(tài)之間的轉(zhuǎn)換需要相對(duì)比較長(zhǎng)的時(shí)間,時(shí)間成本相對(duì)較高,這也是為什么早期的synchronized效率低的原因。慶幸的是在Java 6之后Java官方對(duì)從JVM層面對(duì)synchronized較大優(yōu)化,所以現(xiàn)在的synchronized鎖效率也優(yōu)化得很不錯(cuò)了,Java 6之后,為了減少獲得鎖和釋放鎖所帶來(lái)的性能消耗,引入了輕量級(jí)鎖和偏向鎖。
Java虛擬機(jī)對(duì)synchronized的優(yōu)化
鎖的狀態(tài)總共有四種,無(wú)鎖狀態(tài)、偏向鎖、輕量級(jí)鎖和重量級(jí)鎖。隨著鎖的競(jìng)爭(zhēng),鎖可以從偏向鎖升級(jí)到輕量級(jí)鎖,再升級(jí)的重量級(jí)鎖,但是鎖的升級(jí)是單向的,也就是說(shuō)只能從低到高升級(jí),不會(huì)出現(xiàn)鎖的降級(jí),
無(wú)鎖
可以看到mark word 里面此時(shí)存了
- 鎖狀態(tài)
- 對(duì)象的hashcode
- 對(duì)象的分代年齡,這里用于垃圾回收
- 是否偏向鎖:0否1是
- 鎖標(biāo)志位:01
偏向鎖
在jdk1.6后被提出:在大多數(shù)情況下,鎖并不存在競(jìng)爭(zhēng),一把鎖往往是同一個(gè)線程獲得的,并不需要加鎖和解鎖。因此為了減少同一線程獲取鎖(會(huì)涉及到一些CAS操作,耗時(shí))的代價(jià)而引入偏向鎖。**偏向鎖的核心思想是,如果一個(gè)線程獲得了鎖,那么鎖就進(jìn)入偏向模式,此時(shí)Mark Word 的結(jié)構(gòu)也變?yōu)槠蜴i結(jié)構(gòu),當(dāng)這個(gè)線程再次請(qǐng)求鎖時(shí),無(wú)需再做任何同步操作,即獲取鎖的過(guò)程,**這樣就省去了大量有關(guān)鎖申請(qǐng)的操作,從而也就提供程序的性能。所以,對(duì)于沒(méi)有鎖競(jìng)爭(zhēng)的場(chǎng)合,偏向鎖有很好的優(yōu)化效果,畢竟極有可能連續(xù)多次是同一個(gè)線程申請(qǐng)相同的鎖。但是對(duì)于鎖競(jìng)爭(zhēng)比較激烈的場(chǎng)合,偏向鎖就失效了,因?yàn)檫@樣場(chǎng)合極有可能每次申請(qǐng)鎖的線程都是不相同的,因此這種場(chǎng)合下不應(yīng)該使用偏向鎖,否則會(huì)得不償失,需要注意的是,偏向鎖失敗后,并不會(huì)立即膨脹為重量級(jí)鎖,而是先升級(jí)為輕量級(jí)鎖。
輕量級(jí)鎖
倘若偏向鎖失敗,虛擬機(jī)并不會(huì)立即升級(jí)為重量級(jí)鎖,它還會(huì)嘗試使用一種稱為輕量級(jí)鎖的優(yōu)化手段(1.6之后加入的),此時(shí)Mark Word 的結(jié)構(gòu)也變?yōu)檩p量級(jí)鎖的結(jié)構(gòu)。輕量級(jí)鎖能夠提升程序性能的依據(jù)是“對(duì)絕大部分的鎖,在整個(gè)同步周期內(nèi)都不存在競(jìng)爭(zhēng)”,注意這是經(jīng)驗(yàn)數(shù)據(jù)。需要了解的是,輕量級(jí)鎖所適應(yīng)的場(chǎng)景是線程交替執(zhí)行同步塊的場(chǎng)合,如果存在同一時(shí)間訪問(wèn)同一鎖的場(chǎng)合,就會(huì)導(dǎo)致輕量級(jí)鎖膨脹為重量級(jí)鎖。
自旋鎖
輕量級(jí)鎖失敗后,虛擬機(jī)為了避免線程真實(shí)地在操作系統(tǒng)層面掛起,還會(huì)進(jìn)行一項(xiàng)稱為自旋鎖的優(yōu)化手段。這是基于在大多數(shù)情況下,線程持有鎖的時(shí)間都不會(huì)太長(zhǎng),如果直接掛起操作系統(tǒng)層面的線程可能會(huì)得不償失,畢竟操作系統(tǒng)實(shí)現(xiàn)線程之間的切換時(shí)需要從用戶態(tài)轉(zhuǎn)換到核心態(tài),這個(gè)狀態(tài)之間的轉(zhuǎn)換需要相對(duì)比較長(zhǎng)的時(shí)間,時(shí)間成本相對(duì)較高,因此自旋鎖會(huì)假設(shè)在不久將來(lái),當(dāng)前的線程可以獲得鎖,因此虛擬機(jī)會(huì)讓當(dāng)前想要獲取鎖的線程做幾個(gè)空循環(huán)(這也是稱為自旋的原因),一般不會(huì)太久,可能是50個(gè)循環(huán)或100循環(huán),在經(jīng)過(guò)若干次循環(huán)后,如果得到鎖,就順利進(jìn)入臨界區(qū)。如果還不能獲得鎖,那就會(huì)將線程在操作系統(tǒng)層面掛起,這就是自旋鎖的優(yōu)化方式,這種方式確實(shí)也是可以提升效率的。最后沒(méi)辦法也就只能升級(jí)為重量級(jí)鎖了。
鎖消除
消除鎖是虛擬機(jī)另外一種鎖的優(yōu)化,這種優(yōu)化更徹底,Java虛擬機(jī)在JIT編譯時(shí)(可以簡(jiǎn)單理解為當(dāng)某段代碼即將第一次被執(zhí)行時(shí)進(jìn)行編譯,又稱即時(shí)編譯),通過(guò)對(duì)運(yùn)行上下文的掃描,去除不可能存在共享資源競(jìng)爭(zhēng)的鎖,通過(guò)這種方式消除沒(méi)有必要的鎖,可以節(jié)省毫無(wú)意義的請(qǐng)求鎖時(shí)間,如下StringBuffer的append是一個(gè)同步方法,但是在add方法中的StringBuffer屬于一個(gè)局部變量,并且不會(huì)被其他線程所使用,因此StringBuffer不可能存在共享資源競(jìng)爭(zhēng)的情景,JVM會(huì)自動(dòng)將其鎖消除。
/*** 消除StringBuffer同步鎖*/ public class StringBufferRemoveSync {public void add(String str1, String str2) {//StringBuffer是線程安全,由于sb只會(huì)在append方法中使用,不可能被其他線程引用//因此sb屬于不可能共享的資源,JVM會(huì)自動(dòng)消除內(nèi)部的鎖StringBuffer sb = new StringBuffer();sb.append(str1).append(str2);}public static void main(String[] args) {StringBufferRemoveSync rmsync = new StringBufferRemoveSync();for (int i = 0; i < 10000000; i++) {rmsync.add("abc", "123");}}}鎖的優(yōu)缺點(diǎn)對(duì)比
| 偏向鎖 | 加鎖和解鎖不需要CAS操作,沒(méi)有額外的性能消耗,和執(zhí)行非同步方法相比僅存在納秒級(jí)的差距 | 如果線程間存在鎖競(jìng)爭(zhēng),會(huì)帶來(lái)額外的鎖撤銷的消耗 | 適用于只有一個(gè)線程訪問(wèn)同步快的場(chǎng)景 |
| 輕量級(jí)鎖 | 競(jìng)爭(zhēng)的線程不會(huì)阻塞,提高了響應(yīng)速度 | 如線程成始終得不到鎖競(jìng)爭(zhēng)的線程,使用自旋會(huì)消耗CPU性能 | 追求響應(yīng)時(shí)間,同步快執(zhí)行速度非常快 |
| 重量級(jí)鎖 | 線程競(jìng)爭(zhēng)不適用自旋,不會(huì)消耗CPU | 線程阻塞,響應(yīng)時(shí)間緩慢,在多線程下,頻繁的獲取釋放鎖,會(huì)帶來(lái)巨大的性能消耗 | 追求吞吐量,同步快執(zhí)行速度較長(zhǎng) |
補(bǔ)充知識(shí)點(diǎn)
1.synchronized的可重入性
從互斥鎖的設(shè)計(jì)上來(lái)說(shuō),當(dāng)一個(gè)線程試圖操作一個(gè)由其他線程持有的對(duì)象鎖的臨界資源時(shí),將會(huì)處于阻塞狀態(tài),但當(dāng)一個(gè)線程再次請(qǐng)求自己持有對(duì)象鎖的臨界資源時(shí),這種情況屬于重入鎖,請(qǐng)求將會(huì)成功,在java中synchronized是基于原子性的內(nèi)部鎖機(jī)制,是可重入的,因此在一個(gè)線程調(diào)用synchronized方法的同時(shí)在其方法體內(nèi)部調(diào)用該對(duì)象另一個(gè)synchronized方法,也就是說(shuō)一個(gè)線程得到一個(gè)對(duì)象鎖后再次請(qǐng)求該對(duì)象鎖,是允許的,這就是synchronized的可重入性。如下:
public class AccountingSync implements Runnable{static AccountingSync instance=new AccountingSync();static int i=0;static int j=0;@Overridepublic void run() {for(int j=0;j<1000000;j++){//this,當(dāng)前實(shí)例對(duì)象鎖synchronized(this){i++;increase();//synchronized的可重入性}}}public synchronized void increase(){j++;}public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(instance);Thread t2=new Thread(instance);t1.start();t2.start();t1.join();t2.join();System.out.println(i);} }在獲取當(dāng)前實(shí)例對(duì)象鎖后進(jìn)入synchronized代碼塊執(zhí)行同步代碼,并在代碼塊中調(diào)用了當(dāng)前實(shí)例對(duì)象的另外一個(gè)synchronized方法,再次請(qǐng)求當(dāng)前實(shí)例鎖時(shí),將被允許,進(jìn)而執(zhí)行方法體代碼,這就是重入鎖最直接的體現(xiàn),需要特別注意另外一種情況,當(dāng)子類繼承父類時(shí),子類也是可以通過(guò)可重入鎖調(diào)用父類的同步方法。注意由于synchronized是基于monitor實(shí)現(xiàn)的,因此每次重入,monitor中的計(jì)數(shù)器仍會(huì)加1。
2.synchronized實(shí)現(xiàn)可見(jiàn)性的原理
簡(jiǎn)單地說(shuō)可見(jiàn)性就是把工作內(nèi)存中的數(shù)據(jù)刷入主內(nèi)存,加載數(shù)據(jù)。具體到內(nèi)存屏障
int b = 0;int c = 0;synchronized(this) { -> monitorenter Load內(nèi)存屏障 Acquire內(nèi)存屏障 int a = b; c = 1; => synchronized代碼塊里面還是可能會(huì)發(fā)生指令重排 Release內(nèi)存屏障 } -> monitorexit Store內(nèi)存屏障-
Load屏障的作用是執(zhí)行refresh處理器緩存的操作,說(shuō)白了就是對(duì)別的處理器更新過(guò)來(lái)變量,從其他處理器的高速緩存(或者主內(nèi)存) 加載數(shù)據(jù)到自己的高速緩存來(lái),確保自己看到的是最新的數(shù)據(jù)。
-
Store屏障的作用是執(zhí)行flush處理器緩存的操作,說(shuō)白了就是把自己當(dāng)前處理器更新的變量的值,都刷新到高速緩存(或者主內(nèi)存)里去
基于synchronized代碼塊字節(jié)碼層面上來(lái)說(shuō):
-
在moniterenter指令之后,加入了一個(gè)load屏障,執(zhí)行一個(gè)refresh操作從其他處理器的高速緩存讀取最新數(shù)據(jù)或者從主內(nèi)存加載數(shù)據(jù)
-
在moniterexit指令之后,加入一個(gè)store屏障,執(zhí)行flush操作,把最新值寫入高速緩存或者主內(nèi)存
3.synchronized實(shí)現(xiàn)有序性的原理
如上面代碼所示
- 在monitorenter指令之后,Load屏障之后,會(huì)加一個(gè)Acquire屏障,這個(gè)屏障的作用是禁止讀操作和讀寫操作之間發(fā)生指令重排序。
- 在monitorexit指令之前,會(huì)加一個(gè)Release屏障,這個(gè)屏障的作用是禁止寫操作和讀寫操作之間發(fā)生重排序。
所以說(shuō),通過(guò) Acquire屏障和Release屏障,就可以讓synchronzied保證有序性,只有synchronized內(nèi)部的指令可以重排序,但是絕對(duì) 不會(huì)跟外部的指令發(fā)生重排序。
鞏固提升
找了幾個(gè)例子,鞏固一下上面學(xué)的,看一下能不能想出來(lái)執(zhí)行順序呢!
案例一
public class SynchronizedObjectLock implements Runnable {static SynchronizedObjectLock instence = new SynchronizedObjectLock();@Overridepublic void run() {// 同步代碼塊形式——鎖為this,兩個(gè)線程使用的鎖是一樣的,線程1必須要等到線程0釋放了該鎖后,才能執(zhí)行synchronized (this) {System.out.println("我是線程" + Thread.currentThread().getName());try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "結(jié)束");}}public static void main(String[] args) {Thread t1 = new Thread(instence);Thread t2 = new Thread(instence);t1.start();t2.start();} }// 執(zhí)行結(jié)果 /** 我是線程Thread-0 Thread-0結(jié)束 我是線程Thread-1 Thread-1結(jié)束 **/案例二
public class SynchronizedObjectLock implements Runnable {static SynchronizedObjectLock instence = new SynchronizedObjectLock();// 創(chuàng)建2把鎖Object block1 = new Object();Object block2 = new Object();@Overridepublic void run() {// 這個(gè)代碼塊使用的是第一把鎖,當(dāng)他釋放后,后面的代碼塊由于使用的是第二把鎖,因此可以馬上執(zhí)行synchronized (block1) {System.out.println("block1鎖,我是線程" + Thread.currentThread().getName());try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("block1鎖,"+Thread.currentThread().getName() + "結(jié)束");}synchronized (block2) {System.out.println("block2鎖,我是線程" + Thread.currentThread().getName());try {Thread.sleep(3000);//sleep方法并不會(huì)失去鎖。} catch (InterruptedException e) {e.printStackTrace();}System.out.println("block2鎖,"+Thread.currentThread().getName() + "結(jié)束");}}public static void main(String[] args) {Thread t1 = new Thread(instence);Thread t2 = new Thread(instence);t1.start();t2.start();} }// 執(zhí)行結(jié)果 /** block1鎖,我是線程Thread-0 block1鎖,Thread-0結(jié)束 block2鎖,我是線程Thread-0 //可以看到當(dāng)?shù)谝粋€(gè)線程在執(zhí)行完第一段同步代碼塊之后,第二個(gè)同步代碼塊可以馬上得到執(zhí)行,因?yàn)樗麄兪褂玫逆i不是同一把 block1鎖,我是線程Thread-1 block1鎖,Thread-1結(jié)束 block2鎖,Thread-0結(jié)束 block2鎖,我是線程Thread-1 block2鎖,Thread-1結(jié)束 **/方法鎖形式:synchronized修飾普通方法,鎖對(duì)象默認(rèn)為this
//當(dāng)前線程的鎖便是實(shí)例對(duì)象 //當(dāng)一個(gè)線程獲取了該對(duì)象的鎖之后,其他線程無(wú)法獲取該對(duì)象的鎖,所以無(wú)法訪問(wèn)該對(duì)象的其他synchronized實(shí)例方法 public class SynchronizedObjectLock implements Runnable {static SynchronizedObjectLock instence = new SynchronizedObjectLock();@Overridepublic void run() {method();}public synchronized void method() {System.out.println("我是線程" + Thread.currentThread().getName());try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "結(jié)束");}public static void main(String[] args) {Thread t1 = new Thread(instence);Thread t2 = new Thread(instence);t1.start();t2.start();} }// 執(zhí)行結(jié)果 /** 我是線程Thread-1 Thread-1結(jié)束 我是線程Thread-0 Thread-0結(jié)束 **/方法鎖形式:synchronized修飾普通方法,鎖對(duì)象默認(rèn)為this
//t1和t2對(duì)應(yīng)的this是兩個(gè)不同的實(shí)例,持有鎖不同 普通鎖只是當(dāng)前實(shí)例 public class SynchronizedObjectLock implements Runnable {static SynchronizedObjectLock instence1 = new SynchronizedObjectLock();static SynchronizedObjectLock instence2 = new SynchronizedObjectLock();@Overridepublic void run() {method();}// synchronized用在普通方法上,默認(rèn)的鎖就是this,當(dāng)前實(shí)例public synchronized void method() {System.out.println("我是線程" + Thread.currentThread().getName());try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "結(jié)束");}public static void main(String[] args) {// t1和t2對(duì)應(yīng)的this是兩個(gè)不同的實(shí)例,所以代碼不會(huì)串行Thread t1 = new Thread(instence1);Thread t2 = new Thread(instence2);t1.start();t2.start();} }// 執(zhí)行結(jié)果 /** 我是線程Thread-0 我是線程Thread-1 Thread-1結(jié)束 Thread-0結(jié)束 **/類鎖形式
public class SynchronizedObjectLock implements Runnable {static SynchronizedObjectLock instence1 = new SynchronizedObjectLock();static SynchronizedObjectLock instence2 = new SynchronizedObjectLock();@Overridepublic void run() {method();}// synchronized用在靜態(tài)方法上,默認(rèn)的鎖就是當(dāng)前所在的Class類,所以無(wú)論是哪個(gè)線程訪問(wèn)它,需要的鎖都只有一把public static synchronized void method() {System.out.println("我是線程" + Thread.currentThread().getName());try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "結(jié)束");}public static void main(String[] args) {Thread t1 = new Thread(instence1);Thread t2 = new Thread(instence2);t1.start();t2.start();} } // 執(zhí)行結(jié)果 /** 我是線程Thread-0 Thread-0結(jié)束 我是線程Thread-1 Thread-1結(jié)束 **/同步代碼塊
public class SynchronizedObjectLock implements Runnable {static SynchronizedObjectLock instence1 = new SynchronizedObjectLock();static SynchronizedObjectLock instence2 = new SynchronizedObjectLock();@Overridepublic void run() {// 所有線程需要的鎖都是同一把synchronized(SynchronizedObjectLock.class){System.out.println("我是線程" + Thread.currentThread().getName());try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "結(jié)束");}}public static void main(String[] args) {Thread t1 = new Thread(instence1);Thread t2 = new Thread(instence2);t1.start();t2.start();} } // 執(zhí)行結(jié)果 /** 我是線程Thread-0 Thread-0結(jié)束 我是線程Thread-1 Thread-1結(jié)束 **/標(biāo)注:在學(xué)習(xí)中需要修改的內(nèi)容以及筆記全在這里 www.javanode.cn,謝謝!有任何不妥的地方望糾正
總結(jié)
以上是生活随笔為你收集整理的1202年最新最详细最全的synchronized知识详解的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 最新详细的JMM内存模型(三天熬夜血肝)
- 下一篇: Window下VS运行达梦DPI