1.為什么要使用synchronized
在并發(fā)編程中存在線程安全問題,主要原因有:1.存在共享數(shù)據(jù) 2.多線程共同操作共享數(shù)據(jù)。關(guān)鍵字synchronized可以保證在同一時刻,只有一個線程可以執(zhí)行某個方法或某個代碼塊,同時synchronized可以保證一個線程的變化可見(可見性),即可以代替volatile。synchronized是java1.5之前默認(rèn)的同步方式,在1.6性能進(jìn)行優(yōu)化,某些場景下性能并不低于lock等JUC模塊。
2.實現(xiàn)原理
synchronized可以保證方法或者代碼塊在運行時,同一時刻只有一個方法可以進(jìn)入到臨界區(qū),同時它還可以保證共享變量的內(nèi)存可見性,由虛擬機JVM操作系統(tǒng)級鎖實現(xiàn),jvm基于進(jìn)入和退出Monitor對象來實現(xiàn)方法同步和代碼塊同步,在HotSpot JVM實現(xiàn)中,鎖有個專門的名字:對象監(jiān)視器(管程)。
3.synchronized的三種應(yīng)用方式
Java中每一個對象都可以作為鎖,這是synchronized實現(xiàn)同步的基礎(chǔ):
普通同步方法(實例方法),鎖是當(dāng)前實例對象 ,進(jìn)入同步代碼前要獲得當(dāng)前實例的鎖
靜態(tài)同步方法,鎖是當(dāng)前類的class對象 ,進(jìn)入同步代碼前要獲得當(dāng)前類對象的鎖
同步方法塊,鎖是括號里面的對象,對給定對象加鎖,進(jìn)入同步代碼庫前要獲得給定對象的鎖。
4.JVM對synchronized的鎖優(yōu)化
Synchronized是通過對象內(nèi)部的一個叫做監(jiān)視器鎖(monitor)來實現(xiàn)的,監(jiān)視器鎖本質(zhì)又是依賴于底層的操作系統(tǒng)的Mutex Lock(互斥鎖)來實現(xiàn)的。而操作系統(tǒng)實現(xiàn)線程之間的切換需要從用戶態(tài)轉(zhuǎn)換到核心態(tài),這個成本非常高,狀態(tài)之間的轉(zhuǎn)換需要相對比較長的時間,這就是為什么Synchronized效率低的原因。因此,這種依賴于操作系統(tǒng)Mutex Lock所實現(xiàn)的鎖我們稱之為“重量級鎖”。
Java SE 1.6為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”:鎖一共有4種狀態(tài),級別從低到高依次是:無鎖狀態(tài)、偏向鎖狀態(tài)、輕量級鎖狀態(tài)和重量級鎖狀態(tài)。鎖可以升級但不能降級。
1、偏向鎖
偏向鎖是JDK1.6中引用的優(yōu)化,它的目的是消除數(shù)據(jù)在無競爭情況下的同步原語,進(jìn)一步提高程序的性能。
偏向鎖的獲?。?/p>
判斷是否為可偏向狀態(tài)
如果為可偏向狀態(tài),則判斷線程ID是否是當(dāng)前線程,如果是進(jìn)入同步塊;
如果線程ID并未指向當(dāng)前線程,利用CAS操作競爭鎖,如果競爭成功,將Mark Word中線程ID更新為當(dāng)前線程ID,進(jìn)入同步塊
如果競爭失敗,等待全局安全點,準(zhǔn)備撤銷偏向鎖,根據(jù)線程是否處于活動狀態(tài),決定是轉(zhuǎn)換為無鎖狀態(tài)還是升級為輕量級鎖。
當(dāng)鎖對象第一次被線程獲取的時候,虛擬機會把對象頭中的標(biāo)志位設(shè)置為“01”,即偏向模式。同時使用CAS操作把獲取到這個鎖的線程ID記錄在對象的Mark Word中,如果CAS操作成功。持有偏向鎖的線程以后每次進(jìn)入這個鎖相關(guān)的同步塊時,虛擬機都可以不再進(jìn)行任何同步操作。
偏向鎖的釋放:
偏向鎖使用了遇到競爭才釋放鎖的機制。偏向鎖的撤銷需要等待全局安全點,然后它會首先暫停擁有偏向鎖的線程,然后判斷線程是否還活著,如果線程還活著,則升級為輕量級鎖,否則,將鎖設(shè)置為無鎖狀態(tài)。
2、輕量級鎖
輕量級鎖也是在JDK1.6中引入的新型鎖機制。它不是用來替換重量級鎖的,它的本意是在沒有多線程競爭的情況下,減少傳統(tǒng)的重量級鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能消耗。
加鎖過程:
在代碼進(jìn)入同步塊的時候,如果此對象沒有被鎖定(鎖標(biāo)志位為“01”狀態(tài)),虛擬機首先在當(dāng)前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用于存儲對象目前Mark Word的拷貝(官方把這份拷貝加了一個Displaced前綴,即Displaced Mark Word)。然后虛擬機使用CAS操作嘗試將對象的Mark Word更新為指向鎖記錄(Lock Record)的指針。如果更新成功,那么這個線程就擁有了該對象的鎖,并且對象的Mark Word標(biāo)志位轉(zhuǎn)變?yōu)椤?0”,即表示此對象處于輕量級鎖定狀態(tài);如果更新失敗,虛擬機首先會檢查對象的Mark Word是否指向當(dāng)前線程的棧幀,如果說明當(dāng)前線程已經(jīng)擁有了這個對象的鎖,那就可以直接進(jìn)入同步塊中執(zhí)行,否則說明這個鎖對象已經(jīng)被其他線程占有了。如果有兩條以上的線程競爭同一個鎖,那輕量級鎖不再有效,要膨脹為重量級鎖,鎖標(biāo)志變?yōu)椤?0”,Mark Word中存儲的就是指向重量級鎖的指針,而后面等待的線程也要進(jìn)入阻塞狀態(tài)。
解鎖過程:
如果對象的Mark Word仍然指向線程的鎖記錄,那就用CAS操作將對象當(dāng)前的Mark Word與線程棧幀中的Displaced Mark Word交換回來,如果替換成功,整個同步過程就完成了。如果替換失敗,說明有其他線程嘗試過獲取該鎖,那就要在釋放鎖的同時,喚醒被掛起的線程。
如果沒有競爭,輕量級鎖使用CAS操作避免了使用互斥量的開銷,但如果存在競爭,除了互斥量的開銷外,還額外發(fā)生了CAS操作,因此在有競爭的情況下,輕量級鎖比傳統(tǒng)重量級鎖開銷更大。
3、重量級鎖
Synchronized的重量級鎖是通過對象內(nèi)部的一個叫做監(jiān)視器鎖(monitor)來實現(xiàn)的,監(jiān)視器鎖本質(zhì)又是依賴于底層的操作系統(tǒng)的Mutex Lock(互斥鎖)來實現(xiàn)的。而操作系統(tǒng)實現(xiàn)線程之間的切換需要從用戶態(tài)轉(zhuǎn)換到核心態(tài),這個成本非常高,狀態(tài)之間的轉(zhuǎn)換需要相對比較長的時間,這就是為什么Synchronized效率低的原因。
4、自旋鎖
互斥同步對性能影響最大的是阻塞的實現(xiàn),掛起線程和恢復(fù)線程的操作都需要轉(zhuǎn)入到內(nèi)核態(tài)中完成,這些操作給系統(tǒng)的并發(fā)性能帶來很大的壓力。
于是在阻塞之前,我們讓線程執(zhí)行一個忙循環(huán)(自旋),看看持有鎖的線程是否釋放鎖,如果很快釋放鎖,則沒有必要進(jìn)行阻塞。
5、鎖消除
鎖消除是指虛擬機即時編譯器(JIT)在運行時,對一些代碼上要求同步,但是檢測到不可能發(fā)生數(shù)據(jù)競爭的鎖進(jìn)行消除。
6、鎖粗化
如果虛擬機檢測到有這樣一串零碎的操作都對同一個對象加鎖,將會把加鎖同步的范圍擴展(粗化)到整個操作序列的外部。
Synchronized是Java中解決并發(fā)問題的一種最常用最簡單的方法 ,他可以確保線程互斥的訪問同步代碼
因為synchronized關(guān)鍵字涉及到鎖的概念,所以先來了解一些相關(guān)的鎖知識。
5.說明和舉例
java的內(nèi)置鎖:每個java對象都可以用做一個實現(xiàn)同步的鎖,這些鎖成為內(nèi)置鎖。線程進(jìn)入同步代碼塊或方法的時候會自動獲得該鎖,在退出同步代碼塊或方法時會釋放該鎖。獲得內(nèi)置鎖的唯一途徑就是進(jìn)入這個鎖的保護(hù)的同步代碼塊或方法。java內(nèi)置鎖是一個互斥鎖,這就是意味著最多只有一個線程能夠獲得該鎖,當(dāng)線程A嘗試去獲得線程B持有的內(nèi)置鎖時,線程A必須等待或者阻塞,知道線程B釋放這個鎖,如果B線程不釋放這個鎖,那么A線程將永遠(yuǎn)等待下去
java的對象鎖和類鎖:java的對象鎖和類鎖在鎖的概念上基本上和內(nèi)置鎖是一致的,但是,兩個鎖實際是有很大的區(qū)別的,對象鎖是用于對象實例方法,或者一個對象實例上的,類鎖是用于類的靜態(tài)方法或者一個類的class對象上的。我們知道,類的對象實例可以有很多個,但是每個類只有一個class對象,所以不同對象實例的對象鎖是互不干擾的,但是每個類只有一個類鎖。但是有一點必須注意的是,其實類鎖只是一個概念上的東西,并不是真實存在的,它只是用來幫助我們理解鎖定實例方法和靜態(tài)方法的區(qū)別的上面已經(jīng)對鎖的一些概念有了一點了解,下面探討synchronized關(guān)鍵字的用法。synchronized的用法:synchronized修飾方法和synchronized修飾代碼塊。下面分別分析這兩種用法在對象鎖和類鎖上的效果。
對象鎖的synchronized修飾方法和代碼塊:
publicclassTestSynchronized {publicvoidtest1()? ? {? ? ? ? ? synchronized(this)? ? ? ? ? {inti =5;while( i-- >0)? ? ? ? ? ? ? {? ? ? ? ? ? ? ? ? ? System.out.println(Thread.currentThread().getName() +" : "+ i);try{? ? ? ? ? ? ? ? ? ? ? ? ? Thread.sleep(500);? ? ? ? ? ? ? ? ? ? }catch(InterruptedException ie)? ? ? ? ? ? ? ? ? ? {? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? }? ? ? ? ? }? ? ? }publicsynchronizedvoidtest2()? ? {inti =5;while( i-- >0)? ? ? ? ? {? ? ? ? ? ? ? ? System.out.println(Thread.currentThread().getName() +" : "+ i);try{? ? ? ? ? ? ? ? ? ? Thread.sleep(500);? ? ? ? ? ? ? ? }catch(InterruptedException ie)? ? ? ? ? ? ? {? ? ? ? ? ? ? ? }? ? ? ? ? }? ? ? }publicstaticvoidmain(String[] args)? ? {? ? ? ? ? final TestSynchronized myt2 =newTestSynchronized();? ? ? ? ? Thread test1 =newThread(newRunnable() {publicvoidrun() {? myt2.test1();? }? },"test1");? ? ? ? ? Thread test2 =newThread(newRunnable() {publicvoidrun() { myt2.test2();? }? },"test2");? ? ? ? ? test1.start();;? ? ? ? ? test2.start();//? ? ? ? TestRunnable tr=new TestRunnable();
//? ? ? ? Thread test3=new Thread(tr);
//? ? ? ? test3.start();} }? test2 :4test2 :3test2 :2test2 :1test2 :0test1 :4test1 :3test1 :2test1 :1test1 :0
上述的代碼,第一個方法時用了同步代碼塊的方式進(jìn)行同步,傳入的對象實例是this,表明是當(dāng)前對象,當(dāng)然,如果需要同步其他對象實例,也不可傳入其他對象的實例;第二個方法是修飾方法的方式進(jìn)行同步。因為第一個同步代碼塊傳入的this,所以兩個同步代碼所需要獲得的對象鎖都是同一個對象鎖,下面main方法時分別開啟兩個線程,分別調(diào)用test1和test2方法,那么兩個線程都需要獲得該對象鎖,另一個線程必須等待。上面也給出了運行的結(jié)果可以看到:直到test2線程執(zhí)行完畢,釋放掉鎖,test1線程才開始執(zhí)行。(可能這個結(jié)果有人會有疑問,代碼里面明明是先開啟test1線程,為什么先執(zhí)行的是test2呢?這是因為java編譯器在編譯成字節(jié)碼的時候,會對代碼進(jìn)行一個重排序,也就是說,編譯器會根據(jù)實際情況對代碼進(jìn)行一個合理的排序,編譯前代碼寫在前面,在編譯后的字節(jié)碼不一定排在前面,所以這種運行結(jié)果是正常的, 這里是題外話,最主要是檢驗synchronized的用法的正確性)
如果我們把test2方法的synchronized關(guān)鍵字去掉,執(zhí)行結(jié)果會如何呢?
test1 : 4test2 : 4test2 : 3test1 : 3test1 : 2test2 : 2test2 : 1test1 : 1test2 : 0test1 : 0
上面是執(zhí)行結(jié)果,我們可以看到,結(jié)果輸出是交替著進(jìn)行輸出的,這是因為,某個線程得到了對象鎖,但是另一個線程還是可以訪問沒有進(jìn)行同步的方法或者代碼。進(jìn)行了同步的方法(加鎖方法)和沒有進(jìn)行同步的方法(普通方法)是互不影響的,一個線程進(jìn)入了同步方法,得到了對象鎖,其他線程還是可以訪問那些沒有同步的方法(普通方法)。這里涉及到內(nèi)置鎖的一個概念(此概念出自java并發(fā)編程實戰(zhàn)第二章):對象的內(nèi)置鎖和對象的狀態(tài)之間是沒有內(nèi)在的關(guān)聯(lián)的,雖然大多數(shù)類都將內(nèi)置鎖用做一種有效的加鎖機制,但對象的域并不一定通過內(nèi)置鎖來保護(hù)。當(dāng)獲取到與對象關(guān)聯(lián)的內(nèi)置鎖時,并不能阻止其他線程訪問該對象,當(dāng)某個線程獲得對象的鎖之后,只能阻止其他線程獲得同一個鎖。之所以每個對象都有一個內(nèi)置鎖,是為了免去顯式地創(chuàng)建鎖對象。所以synchronized只是一個內(nèi)置鎖的加鎖機制,當(dāng)某個方法加上synchronized關(guān)鍵字后,就表明要獲得該內(nèi)置鎖才能執(zhí)行,并不能阻止其他線程訪問不需要獲得該內(nèi)置鎖的方法。類鎖的修飾(靜態(tài))方法和代碼塊:
publicclassTestSynchronized {publicvoidtest1()? ? {? ? ? ? ? synchronized(TestSynchronized.class)? ? ? ? ? {inti =5;while( i-- >0)? ? ? ? ? ? ? {? ? ? ? ? ? ? ? ? ? System.out.println(Thread.currentThread().getName() +" : "+ i);try{? ? ? ? ? ? ? ? ? ? ? ? ? Thread.sleep(500);? ? ? ? ? ? ? ? ? ? }catch(InterruptedException ie)? ? ? ? ? ? ? ? ? ? {? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? }? ? ? ? ? }? ? ? }publicstaticsynchronizedvoidtest2()? ? {inti =5;while( i-- >0)? ? ? ? ? {? ? ? ? ? ? ? ? System.out.println(Thread.currentThread().getName() +" : "+ i);try{? ? ? ? ? ? ? ? ? ? Thread.sleep(500);? ? ? ? ? ? ? ? }catch(InterruptedException ie)? ? ? ? ? ? ? {? ? ? ? ? ? ? ? }? ? ? ? ? }? ? ? }publicstaticvoidmain(String[] args)? ? {? ? ? ? ? final TestSynchronized myt2 =newTestSynchronized();? ? ? ? ? Thread test1 =newThread(newRunnable() {publicvoidrun() {? myt2.test1();? }? },"test1");? ? ? ? ? Thread test2 =newThread(newRunnable() {publicvoidrun() { TestSynchronized.test2();? }? },"test2");? ? ? ? ? test1.start();? ? ? ? ? test2.start();//? ? ? ? TestRunnable tr=new TestRunnable();
//? ? ? ? Thread test3=new Thread(tr);
//? ? ? ? test3.start();} } test1 :4test1 :3test1 :2test1 :1test1 :0test2 :4test2 :3test2 :2test2 :1test2 :0
其實,類鎖修飾方法和代碼塊的效果和對象鎖是一樣的,因為類鎖只是一個抽象出來的概念,只是為了區(qū)別靜態(tài)方法的特點,因為靜態(tài)方法是所有對象實例共用的,所以對應(yīng)著synchronized修飾的靜態(tài)方法的鎖也是唯一的,所以抽象出來個類鎖。其實這里的重點在下面這塊代碼,synchronized同時修飾靜態(tài)和非靜態(tài)方法
publicclassTestSynchronized {publicsynchronizedvoidtest1()? ? {inti =5;while( i-- >0)? ? ? ? ? ? ? {? ? ? ? ? ? ? ? ? ? System.out.println(Thread.currentThread().getName() +" : "+ i);try{? ? ? ? ? ? ? ? ? ? ? ? ? Thread.sleep(500);? ? ? ? ? ? ? ? ? ? }catch(InterruptedException ie)? ? ? ? ? ? ? ? ? ? {? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? }? ? ? }publicstaticsynchronizedvoidtest2()? ? {inti =5;while( i-- >0)? ? ? ? ? {? ? ? ? ? ? ? ? System.out.println(Thread.currentThread().getName() +" : "+ i);try{? ? ? ? ? ? ? ? ? ? Thread.sleep(500);? ? ? ? ? ? ? ? }catch(InterruptedException ie)? ? ? ? ? ? ? {? ? ? ? ? ? ? ? }? ? ? ? ? }? ? ? }publicstaticvoidmain(String[] args)? ? {? ? ? ? ? final TestSynchronized myt2 =newTestSynchronized();? ? ? ? ? Thread test1 =newThread(newRunnable() {publicvoidrun() {? myt2.test1();? }? },"test1");? ? ? ? ? Thread test2 =newThread(newRunnable() {publicvoidrun() { TestSynchronized.test2();? }? },"test2");? ? ? ? ? test1.start();? ? ? ? ? test2.start();//? ? ? ? TestRunnable tr=new TestRunnable();
//? ? ? ? Thread test3=new Thread(tr);
//? ? ? ? test3.start();} } test1 :4test2 :4test1 :3test2 :3test2 :2test1 :2test2 :1test1 :1test1 :0test2 :0