先看一段synchronized 的詳解:
synchronized 是 java語(yǔ)言的關(guān)鍵字,當(dāng)它用來(lái)修飾一個(gè)方法或者一個(gè)代碼塊的時(shí)候,能夠保證在同一時(shí)刻最多只有一個(gè)線程執(zhí)行該段代碼。
一、當(dāng)兩個(gè)并發(fā)線程訪問(wèn)同一個(gè)對(duì)象object中的這個(gè)synchronized(this)同步代碼塊時(shí),一個(gè)時(shí)間內(nèi)只能有一個(gè)線程得到執(zhí)行。另一個(gè)線程必須等待當(dāng)前線程執(zhí)行完這個(gè)代碼塊以后才能執(zhí)行該代碼塊。
二、然而,當(dāng)一個(gè)線程訪問(wèn)object的一個(gè)synchronized(this)同步代碼塊時(shí),另一個(gè)線程仍然可以訪問(wèn)該object中的非synchronized(this)同步代碼塊。
三、尤其關(guān)鍵的是,當(dāng)一個(gè)線程訪問(wèn)object的一個(gè)synchronized(this)同步代碼塊時(shí),其他線程對(duì)object中所有其它synchronized(this)同步代碼塊的訪問(wèn)將被阻塞。
四、第三個(gè)例子同樣適用其它同步代碼塊。也就是說(shuō),當(dāng)一個(gè)線程訪問(wèn)object的一個(gè)synchronized(this)同步代碼塊時(shí),它就獲得了這個(gè)object的對(duì)象鎖。結(jié)果,其它線程對(duì)該object對(duì)象所有同步代碼部分的訪問(wèn)都被暫時(shí)阻塞。
五、以上規(guī)則對(duì)其它對(duì)象鎖同樣適用.
簡(jiǎn)單來(lái)說(shuō), synchronized就是為當(dāng)前的線程聲明一個(gè)鎖, 擁有這個(gè)鎖的線程可以執(zhí)行區(qū)塊里面的指令, 其他的線程只能等待獲取鎖, 然后才能相同的操作.
這個(gè)很好用, 但是筆者遇到另一種比較奇葩的情況.
1. 在同一類中, 有兩個(gè)方法是用了synchronized關(guān)鍵字聲明
2. 在執(zhí)行完其中一個(gè)方法的時(shí)候, 需要等待另一個(gè)方法(異步線程回調(diào))也執(zhí)行完, 所以用了一個(gè)countDownLatch來(lái)做等待
3. 代碼解構(gòu)如下:
synchronized void a(){ countDownLatch = new CountDownLatch(1); // do someing countDownLatch.await();}synchronized void b(){ countDownLatch.countDown();}其中
a方法由主線程執(zhí)行, b方法由異步線程執(zhí)行后回調(diào)
執(zhí)行結(jié)果是:
主線程執(zhí)行 a方法后開(kāi)始卡住, 不再往下做, 任你等多久都沒(méi)用.
這是一個(gè)很經(jīng)典的死鎖問(wèn)題
a等待b執(zhí)行, 其實(shí)不要看b是回調(diào)的, b也在等待a執(zhí)行. 為什么呢? synchronized 起了作用.
一般來(lái)說(shuō), 我們要synchronized一段代碼塊的時(shí)候, 我們需要使用一個(gè)共享變量來(lái)鎖住, 比如:
byte[] mutex = new byte[0];void a1(){ synchronized(mutex){ //dosomething }}void b1(){ synchronized(mutex){ // dosomething }}如果把a(bǔ)方法和b方法的內(nèi)容分別遷移到 a1和b1 方法的synchronized塊里面, 就很好理解了.
a1執(zhí)行完后會(huì)間接等待(countDownLatch)b1方法執(zhí)行.
然而由于 a1 中的mutex并沒(méi)有釋放, 就開(kāi)始等待b1了, 這時(shí)候, 即使是異步的回調(diào)b1方法, 由于需要等待mutex釋放鎖, 所以b方法并不會(huì)執(zhí)行.
于是就引起了死鎖!
而這里的synchronized關(guān)鍵字放在方法前面, 起的作用就是一樣的. 只是java語(yǔ)言幫你隱去了mutex的聲明和使用而已. 同一個(gè)對(duì)象中的synchronized 方法用到的mutex是相同的, 所以即使是異步回調(diào), 也會(huì)引起死鎖, 所以要注意這個(gè)問(wèn)題. 這種級(jí)別的錯(cuò)誤是屬于synchronized關(guān)鍵字使用不當(dāng). 不要亂用, 而且要用對(duì).
那么這樣的 隱形的mutex 對(duì)象究竟是 什么呢?
很容易想到的就是 實(shí)例本身. 因?yàn)檫@樣就不用去定義新的對(duì)象了做鎖了. 為了證明這個(gè)設(shè)想, 可以寫(xiě)一段程序來(lái)證明.
思路很簡(jiǎn)單, 定義一個(gè)類, 有兩個(gè)方法, 一個(gè)方法聲明為 synchronized, 一個(gè)在 方法體里面使用synchronized(this), 然后啟動(dòng)兩個(gè)線程, 來(lái)分別調(diào)用這兩個(gè)方法, 如果兩個(gè)方法之間發(fā)生鎖競(jìng)爭(zhēng)(等待)的話, 就可以說(shuō)明 方法聲明的 synchronized 中的隱形的mutex其實(shí)就是 實(shí)例本身了.
public class MultiThreadSync { public synchronized void m1() throws InterruptedException{ System. out.println("m1 call" ); Thread. sleep(2000); System. out.println("m1 call done" ); } public void m2() throws InterruptedException{ synchronized (this ) { System. out.println("m2 call" ); Thread. sleep(2000); System. out.println("m2 call done" ); } } public static void main(String[] args) { final MultiThreadSync thisObj = new MultiThreadSync(); Thread t1 = new Thread(){ @Override public void run() { try { thisObj.m1(); } catch (InterruptedException e) { e.printStackTrace(); } } }; Thread t2 = new Thread(){ @Override public void run() { try { thisObj.m2(); } catch (InterruptedException e) { e.printStackTrace(); } } }; t1.start(); t2.start(); }}結(jié)果輸出是:
m1 callm1 call donem2 callm2 call done
說(shuō)明方法m2的sync塊等待了m1的執(zhí)行. 這樣就可以證實(shí) 上面的設(shè)想了.
另外需要說(shuō)明的是, 當(dāng)sync加在 static的方法上的時(shí)候, 由于是類級(jí)別的方法, 所以鎖住的對(duì)象是當(dāng)前類的class實(shí)例. 同樣也可以寫(xiě)程序進(jìn)行證明.這里略.
所以方法的synchronized 關(guān)鍵字, 在閱讀的時(shí)候可以自動(dòng)替換為synchronized(this){}就很好理解了.
void method(){void synchronized method(){ synchronized(this){ // biz code // biz code} ------>>> } }由Synchronized的內(nèi)存可見(jiàn)性說(shuō)開(kāi)去
在Java中,我們都知道關(guān)鍵字synchronized可以用于實(shí)現(xiàn)線程間的互斥,但我們卻常常忘記了它還有另外一個(gè)作用,那就是確保變量在內(nèi)存的可見(jiàn)性 - 即當(dāng)讀寫(xiě)兩個(gè)線程同時(shí)訪問(wèn)同一個(gè)變量時(shí),synchronized用于確保寫(xiě)線程更新變量后,讀線程再訪問(wèn)該 變量時(shí)可以讀取到該變量最新的值。
比如說(shuō)下面的例子:
public class NoVisibility { private static boolean ready = false; private static int number = 0; private static class ReaderThread extends Thread { @Override public void run() { while (!ready) { Thread.yield(); //交出CPU讓其它線程工作 } System.out.println(number); } } public static void main(String[] args) { new ReaderThread().start(); number = 42; ready = true; }}你認(rèn)為讀線程會(huì)輸出什么? 42? 在正常情況下是會(huì)輸出42. 但是由于重排序問(wèn)題,讀線程還有可能會(huì)輸出0 或者什么都不輸出。
我們知道,編譯器在將Java代碼編譯成字節(jié)碼的時(shí)候可能會(huì)對(duì)代碼進(jìn)行重排序,而CPU在執(zhí)行機(jī)器指令的時(shí)候也可能會(huì)對(duì)其指令進(jìn)行重排序,只要重排序不會(huì)破壞程序的語(yǔ)義 -
在單一線程中,只要重排序不會(huì)影響到程序的執(zhí)行結(jié)果,那么就不能保證其中的操作一定按照程序?qū)懚ǖ捻樞驁?zhí)行,即使重排序可能會(huì)對(duì)其它線程產(chǎn)生明顯的影響。
這也就是說(shuō),語(yǔ)句"ready=true"的執(zhí)行有可能要優(yōu)先于語(yǔ)句"number=42"的執(zhí)行,這種情況下,讀線程就有可能會(huì)輸出number的默認(rèn)值0.
而在Java內(nèi)存模型下,重排序問(wèn)題是會(huì)導(dǎo)致這樣的內(nèi)存的可見(jiàn)性問(wèn)題的。在Java內(nèi)存模型下,每個(gè)線程都有它自己的工作內(nèi)存(主要是CPU的cache或寄存器),它對(duì)變量的操作都在自己的工作內(nèi)存中進(jìn)行,而線程之間的通信則是通過(guò)主存和線程的工作內(nèi)存之間的同步來(lái)實(shí)現(xiàn)的。
比如說(shuō),對(duì)于上面的例子而言,寫(xiě)線程已經(jīng)成功的將number更新為42,ready更新為true了,但是很有可能寫(xiě)線程只同步了number到主存中(可能是由于CPU的寫(xiě)緩沖導(dǎo)致),導(dǎo)致后續(xù)的讀線程讀取的ready值一直為false,那么上面的代碼就不會(huì)輸出任何數(shù)值。

而如果我們使用了synchronized關(guān)鍵字來(lái)進(jìn)行同步,則不會(huì)存在這樣的問(wèn)題,
public class NoVisibility { private static boolean ready = false; private static int number = 0; private static Object lock = new Object(); private static class ReaderThread extends Thread { @Override public void run() { synchronized (lock) { while (!ready) { Thread.yield(); } System.out.println(number); } } } public static void main(String[] args) { synchronized (lock) { new ReaderThread().start(); number = 42; ready = true; } }}這個(gè)是因?yàn)镴ava內(nèi)存模型對(duì)synchronized語(yǔ)義做了以下的保證,

即當(dāng)ThreadA釋放鎖M時(shí),它所寫(xiě)過(guò)的變量(比如,x和y,存在它工作內(nèi)存中的)都會(huì)同步到主存中,而當(dāng)ThreadB在申請(qǐng)同一個(gè)鎖M時(shí),ThreadB的工作內(nèi)存會(huì)被設(shè)置為無(wú)效,然后ThreadB會(huì)重新從主存中加載它要訪問(wèn)的變量到它的工作內(nèi)存中(這時(shí)x=1,y=1,是ThreadA中修改過(guò)的最新的值)。通過(guò)這樣的方式來(lái)實(shí)現(xiàn)ThreadA到ThreadB的線程間的通信。
這實(shí)際上是JSR133定義的其中一條happen-before規(guī)則。JSR133給Java內(nèi)存模型定義以下一組happen-before規(guī)則,
實(shí)際上這組happens-before規(guī)則定義了操作之間的內(nèi)存可見(jiàn)性,如果A操作happens-before B操作,那么A操作的執(zhí)行結(jié)果(比如對(duì)變量的寫(xiě)入)必定在執(zhí)行B操作時(shí)可見(jiàn)。
為了更加深入的了解這些happens-before規(guī)則,我們來(lái)看一個(gè)例子:
//線程A,B共同訪問(wèn)的代碼Object lock = new Object();int a=0;int b=0;int c=0;//線程A,調(diào)用如下代碼synchronized(lock){ a=1; //1 b=2; //2} //3c=3; //4//線程B,調(diào)用如下代碼synchronized(lock){ //5 System.out.println(a); //6 System.out.println(b); //7 System.out.println(c); //8}我們假設(shè)線程A先運(yùn)行,分別給a,b,c三個(gè)變量進(jìn)行賦值(注:變量a,b的賦值是在同步語(yǔ)句塊中進(jìn)行的),然后線程B再運(yùn)行,分別讀取出這三個(gè)變量的值并打印出來(lái)。那么線程B打印出來(lái)的變量a,b,c的值分別是多少?
根據(jù)單線程規(guī)則,在A線程的執(zhí)行中,我們可以得出1操作happens before于2操作,2操作happens before于3操作,3操作happens before于4操作。同理,在B線程的執(zhí)行中,5操作happens before于6操作,6操作happens before于7操作,7操作happens before于8操作。而根據(jù)監(jiān)視器的解鎖和加鎖原則,3操作(解鎖操作)是happens before 5操作的(加鎖操作),再根據(jù)傳遞性 規(guī)則我們可以得出,操作1,2是happens before 操作6,7,8的。
則根據(jù)happens-before的內(nèi)存語(yǔ)義,操作1,2的執(zhí)行結(jié)果對(duì)于操作6,7,8是可見(jiàn)的,那么線程B里,打印的a,b肯定是1和2. 而對(duì)于變量c的操作4,和操作8. 我們并不能根據(jù)現(xiàn)有的happens before規(guī)則推出操作4 happens before于操作8. 所以在線程B中,訪問(wèn)的到c變量有可能還是0,而不是3.
新聞熱點(diǎn)
疑難解答
圖片精選