詳解Java中synchronized關(guān)鍵字的死鎖和內(nèi)存占用問題
先看一段synchronized 的詳解:
synchronized 是 java語言的關(guān)鍵字,當(dāng)它用來修飾一個(gè)方法或者一個(gè)代碼塊的時(shí)候,能夠保證在同一時(shí)刻最多只有一個(gè)線程執(zhí)行該段代碼。
一、當(dāng)兩個(gè)并發(fā)線程訪問同一個(gè)對(duì)象object中的這個(gè)synchronized(this)同步代碼塊時(shí),一個(gè)時(shí)間內(nèi)只能有一個(gè)線程得到執(zhí)行。另一個(gè)線程必須等待當(dāng)前線程執(zhí)行完這個(gè)代碼塊以后才能執(zhí)行該代碼塊。
二、然而,當(dāng)一個(gè)線程訪問object的一個(gè)synchronized(this)同步代碼塊時(shí),另一個(gè)線程仍然可以訪問該object中的非synchronized(this)同步代碼塊。
三、尤其關(guān)鍵的是,當(dāng)一個(gè)線程訪問object的一個(gè)synchronized(this)同步代碼塊時(shí),其他線程對(duì)object中所有其它synchronized(this)同步代碼塊的訪問將被阻塞。
四、第三個(gè)例子同樣適用其它同步代碼塊。也就是說,當(dāng)一個(gè)線程訪問object的一個(gè)synchronized(this)同步代碼塊時(shí),它就獲得了這個(gè)object的對(duì)象鎖。結(jié)果,其它線程對(duì)該object對(duì)象所有同步代碼部分的訪問都被暫時(shí)阻塞。
五、以上規(guī)則對(duì)其它對(duì)象鎖同樣適用.
簡單來說, synchronized就是為當(dāng)前的線程聲明一個(gè)鎖, 擁有這個(gè)鎖的線程可以執(zhí)行區(qū)塊里面的指令, 其他的線程只能等待獲取鎖, 然后才能相同的操作.
這個(gè)很好用, 但是筆者遇到另一種比較奇葩的情況.
1. 在同一類中, 有兩個(gè)方法是用了synchronized關(guān)鍵字聲明
2. 在執(zhí)行完其中一個(gè)方法的時(shí)候, 需要等待另一個(gè)方法(異步線程回調(diào))也執(zhí)行完, 所以用了一個(gè)countDownLatch來做等待
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方法后開始卡住, 不再往下做, 任你等多久都沒用.
這是一個(gè)很經(jīng)典的死鎖問題
a等待b執(zhí)行, 其實(shí)不要看b是回調(diào)的, b也在等待a執(zhí)行. 為什么呢? synchronized 起了作用.
一般來說, 我們要synchronized一段代碼塊的時(shí)候, 我們需要使用一個(gè)共享變量來鎖住, 比如:
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并沒有釋放, 就開始等待b1了, 這時(shí)候, 即使是異步的回調(diào)b1方法, 由于需要等待mutex釋放鎖, 所以b方法并不會(huì)執(zhí)行.
于是就引起了死鎖!
而這里的synchronized關(guān)鍵字放在方法前面, 起的作用就是一樣的. 只是java語言幫你隱去了mutex的聲明和使用而已. 同一個(gè)對(duì)象中的synchronized 方法用到的mutex是相同的, 所以即使是異步回調(diào), 也會(huì)引起死鎖, 所以要注意這個(gè)問題. 這種級(jí)別的錯(cuò)誤是屬于synchronized關(guān)鍵字使用不當(dāng). 不要亂用, 而且要用對(duì).
那么這樣的 隱形的mutex 對(duì)象究竟是 什么呢?
很容易想到的就是 實(shí)例本身. 因?yàn)檫@樣就不用去定義新的對(duì)象了做鎖了. 為了證明這個(gè)設(shè)想, 可以寫一段程序來證明.
思路很簡單, 定義一個(gè)類, 有兩個(gè)方法, 一個(gè)方法聲明為 synchronized, 一個(gè)在 方法體里面使用synchronized(this), 然后啟動(dòng)兩個(gè)線程, 來分別調(diào)用這兩個(gè)方法, 如果兩個(gè)方法之間發(fā)生鎖競(jìng)爭(等待)的話, 就可以說明 方法聲明的 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 call m1 call done m2 call m2 call done
說明方法m2的sync塊等待了m1的執(zhí)行. 這樣就可以證實(shí) 上面的設(shè)想了.
另外需要說明的是, 當(dāng)sync加在 static的方法上的時(shí)候, 由于是類級(jí)別的方法, 所以鎖住的對(duì)象是當(dāng)前類的class實(shí)例. 同樣也可以寫程序進(jìn)行證明.這里略.
所以方法的synchronized 關(guān)鍵字, 在閱讀的時(shí)候可以自動(dòng)替換為synchronized(this){}就很好理解了.
void method(){ void synchronized method(){ synchronized(this){ // biz code // biz code } ------>>> } }
由Synchronized的內(nèi)存可見性說開去
在Java中,我們都知道關(guān)鍵字synchronized可以用于實(shí)現(xiàn)線程間的互斥,但我們卻常常忘記了它還有另外一個(gè)作用,那就是確保變量在內(nèi)存的可見性 - 即當(dāng)讀寫兩個(gè)線程同時(shí)訪問同一個(gè)變量時(shí),synchronized用于確保寫線程更新變量后,讀線程再訪問該 變量時(shí)可以讀取到該變量最新的值。
比如說下面的例子:
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. 但是由于重排序問題,讀線程還有可能會(huì)輸出0 或者什么都不輸出。
我們知道,編譯器在將Java代碼編譯成字節(jié)碼的時(shí)候可能會(huì)對(duì)代碼進(jìn)行重排序,而CPU在執(zhí)行機(jī)器指令的時(shí)候也可能會(huì)對(duì)其指令進(jìn)行重排序,只要重排序不會(huì)破壞程序的語義 -
在單一線程中,只要重排序不會(huì)影響到程序的執(zhí)行結(jié)果,那么就不能保證其中的操作一定按照程序?qū)懚ǖ捻樞驁?zhí)行,即使重排序可能會(huì)對(duì)其它線程產(chǎn)生明顯的影響。
這也就是說,語句"ready=true"的執(zhí)行有可能要優(yōu)先于語句"number=42"的執(zhí)行,這種情況下,讀線程就有可能會(huì)輸出number的默認(rèn)值0.
而在Java內(nèi)存模型下,重排序問題是會(huì)導(dǎo)致這樣的內(nèi)存的可見性問題的。在Java內(nèi)存模型下,每個(gè)線程都有它自己的工作內(nèi)存(主要是CPU的cache或寄存器),它對(duì)變量的操作都在自己的工作內(nèi)存中進(jìn)行,而線程之間的通信則是通過主存和線程的工作內(nèi)存之間的同步來實(shí)現(xiàn)的。
比如說,對(duì)于上面的例子而言,寫線程已經(jīng)成功的將number更新為42,ready更新為true了,但是很有可能寫線程只同步了number到主存中(可能是由于CPU的寫緩沖導(dǎo)致),導(dǎo)致后續(xù)的讀線程讀取的ready值一直為false,那么上面的代碼就不會(huì)輸出任何數(shù)值。
而如果我們使用了synchronized關(guān)鍵字來進(jìn)行同步,則不會(huì)存在這樣的問題,
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語義做了以下的保證,
即當(dāng)ThreadA釋放鎖M時(shí),它所寫過的變量(比如,x和y,存在它工作內(nèi)存中的)都會(huì)同步到主存中,而當(dāng)ThreadB在申請(qǐng)同一個(gè)鎖M時(shí),ThreadB的工作內(nèi)存會(huì)被設(shè)置為無效,然后ThreadB會(huì)重新從主存中加載它要訪問的變量到它的工作內(nèi)存中(這時(shí)x=1,y=1,是ThreadA中修改過的最新的值)。通過這樣的方式來實(shí)現(xiàn)ThreadA到ThreadB的線程間的通信。
這實(shí)際上是JSR133定義的其中一條happen-before規(guī)則。JSR133給Java內(nèi)存模型定義以下一組happen-before規(guī)則,
- 單線程規(guī)則:同一個(gè)線程中的每個(gè)操作都happens-before于出現(xiàn)在其后的任何一個(gè)操作。
- 對(duì)一個(gè)監(jiān)視器的解鎖操作happens-before于每一個(gè)后續(xù)對(duì)同一個(gè)監(jiān)視器的加鎖操作。
- 對(duì)volatile字段的寫入操作happens-before于每一個(gè)后續(xù)的對(duì)同一個(gè)volatile字段的讀操作。
- Thread.start()的調(diào)用操作會(huì)happens-before于啟動(dòng)線程里面的操作。
- 一個(gè)線程中的所有操作都happens-before于其他線程成功返回在該線程上的join()調(diào)用后的所有操作。
- 一個(gè)對(duì)象構(gòu)造函數(shù)的結(jié)束操作happens-before與該對(duì)象的finalizer的開始操作。
- 傳遞性規(guī)則:如果A操作happens-before于B操作,而B操作happens-before與C操作,那么A動(dòng)作happens-before于C操作。
實(shí)際上這組happens-before規(guī)則定義了操作之間的內(nèi)存可見性,如果A操作happens-before B操作,那么A操作的執(zhí)行結(jié)果(比如對(duì)變量的寫入)必定在執(zhí)行B操作時(shí)可見。
為了更加深入的了解這些happens-before規(guī)則,我們來看一個(gè)例子:
//線程A,B共同訪問的代碼 Object lock = new Object(); int a=0; int b=0; int c=0; //線程A,調(diào)用如下代碼 synchronized(lock){ a=1; //1 b=2; //2 } //3 c=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的賦值是在同步語句塊中進(jìn)行的),然后線程B再運(yùn)行,分別讀取出這三個(gè)變量的值并打印出來。那么線程B打印出來的變量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)存語義,操作1,2的執(zhí)行結(jié)果對(duì)于操作6,7,8是可見的,那么線程B里,打印的a,b肯定是1和2. 而對(duì)于變量c的操作4,和操作8. 我們并不能根據(jù)現(xiàn)有的happens before規(guī)則推出操作4 happens before于操作8. 所以在線程B中,訪問的到c變量有可能還是0,而不是3.
相關(guān)文章
Java設(shè)計(jì)模式之迭代器模式_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要介紹了Java設(shè)計(jì)模式之迭代器模式_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理,需要的朋友可以參考下2017-08-08Java時(shí)間轉(zhuǎn)換成unix時(shí)間戳的方法
這篇文章主要為大家詳細(xì)介紹了Java時(shí)間轉(zhuǎn)換成unix時(shí)間戳的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-12-12Java 中組合模型之對(duì)象結(jié)構(gòu)模式的詳解
這篇文章主要介紹了Java 中組合模型之對(duì)象結(jié)構(gòu)模式的詳解的相關(guān)資料,希望通過本文能幫助到大家理解應(yīng)用對(duì)象結(jié)構(gòu)模型,需要的朋友可以參考下2017-09-09java自帶命令行工具jmap、jhat與jinfo的使用實(shí)例代碼詳解
本篇文章主要通過代碼實(shí)例對(duì)java自帶命令行工具jmap、jhat與jinfo的使用做出了詳解,需要的朋友可以參考下2017-04-04詳解Java多線程編程中LockSupport類的線程阻塞用法
LockSupport類提供了park()和unpark()兩個(gè)方法來實(shí)現(xiàn)線程的阻塞和喚醒,下面我們就來詳解Java多線程編程中LockSupport類的線程阻塞用法:2016-07-07java與C 代碼運(yùn)行效率的對(duì)比(整理)
最近和朋友無意間討論起了 有關(guān)java 和C 的 效率問題, (我是java 推介者, 他是 c 語言推介者, 他做的是嵌入式)故,想通過網(wǎng)絡(luò)查詢一下, 總結(jié)一下,兩者到底效率如何,其有何差異,原因又是啥?各種優(yōu)勢(shì)有在何處?2021-04-04Java @Value("${xxx}")取properties時(shí)中文亂碼的解決
這篇文章主要介紹了Java @Value("${xxx}")取properties時(shí)中文亂碼的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07Java面向?qū)ο蟪绦蛟O(shè)計(jì):類的定義,靜態(tài)變量,成員變量,構(gòu)造函數(shù),封裝與私有,this概念與用法詳解
這篇文章主要介紹了Java面向?qū)ο箢惖亩x,靜態(tài)變量,成員變量,構(gòu)造函數(shù),封裝與私有,this概念與用法,較為詳細(xì)的分析了Java類的定義,靜態(tài)變量,成員變量,構(gòu)造函數(shù),封裝,私有等相關(guān)原理、用法及操作注意事項(xiàng),需要的朋友可以參考下2020-04-04