java 多线程同步问题_Java多线程同步问题:一个小Demo完全搞懂
版權(quán)聲明:本文出自汪磊的博客,轉(zhuǎn)載請(qǐng)務(wù)必注明出處。
Java線程系列文章只是自己知識(shí)的總結(jié)梳理,都是最基礎(chǔ)的玩意,已經(jīng)掌握熟練的可以繞過(guò)。
一、一個(gè)簡(jiǎn)單的Demo引發(fā)的血案
關(guān)于線程同步問(wèn)題我們從一個(gè)簡(jiǎn)單的Demo現(xiàn)象說(shuō)起。Demo特別簡(jiǎn)單就是開(kāi)啟兩個(gè)線程打印字符串信息。
OutPutStr類源碼:
1 public classOutPutStr {2
3 public voidout(String str) {4 for (int i = 0; i < str.length(); i++) {5 System.out.print(str.charAt(i));6 }7 System.out.println();8 }9 }
很簡(jiǎn)單吧,就是一個(gè)方法供外界調(diào)用,調(diào)用的時(shí)候傳進(jìn)來(lái)一個(gè)字符串,方法逐個(gè)取出字符串的字符并打印到控制臺(tái)。
接下來(lái),我們看main方法中邏輯:
1 public static voidmain(String[] args) {2 //3 final OutPutStr o = newOutPutStr();4 new Thread(newRunnable() {5
6 @Override7 public voidrun() {8 //9 while(true){10 o.out("111111111111");11 }12 }13 }).start();14 new Thread(newRunnable() {15
16 @Override17 public voidrun() {18 //19 while(true){20 o.out("222222222222");21 }22 }23 }).start();24 }
也很簡(jiǎn)單,就是開(kāi)啟兩個(gè)線程分別調(diào)用OutPutStr中out方法不停打印字符串信息,運(yùn)行程序打印信息如下:
1 222222222222
2 222222222222
3 22222222222111111111
4 2
5 111111111111
6 111111111111
7 1111222222222211111111
8 111111111111
咦?和我們想的不一樣啊,怎么還會(huì)打印出22222222222111111111這樣子的信息,這是怎么回事呢?
二、原因解析
我們知道線程的執(zhí)行是CPU隨機(jī)調(diào)度的,比如我們開(kāi)啟10個(gè)線程,這10個(gè)線程并不是同時(shí)執(zhí)行的,而是CPU快速的在這10個(gè)線程之間切換執(zhí)行,由于切換速度極快使我們感覺(jué)同時(shí)執(zhí)行罷了。發(fā)生上面問(wèn)題的本質(zhì)就是CPU對(duì)線程執(zhí)行的隨機(jī)調(diào)度,比如A線程此時(shí)正在打印信息還沒(méi)打印完畢此時(shí)CPU切換到B線程執(zhí)行了,B線程執(zhí)行完了又切換回A線程執(zhí)行就會(huì)導(dǎo)致上面現(xiàn)象發(fā)生。
線程同步問(wèn)題往往發(fā)生在多個(gè)線程調(diào)用同一方法或者操作同一變量,但是我們要知道其本質(zhì)就是CPU對(duì)線程的隨機(jī)調(diào)度,CPU無(wú)法保證一個(gè)線程執(zhí)行完其邏輯才去調(diào)用另一個(gè)線程執(zhí)行。
三、同步方法解決上述問(wèn)題
既然知道了問(wèn)題發(fā)生的原因,記下來(lái)我們就要想辦法解決問(wèn)題啊,解決的思路就是保證一個(gè)線程在調(diào)用out方法的時(shí)候如果沒(méi)執(zhí)行完那么另一個(gè)不能執(zhí)行此方法,換句話說(shuō)就是只能等待別的線程執(zhí)行完畢才能執(zhí)行。
針對(duì)線程同步問(wèn)題java早就有解決方法了,最簡(jiǎn)單的就是給方法加上synchronized關(guān)鍵字,如下:
1 public synchronized voidout(String str) {2 for (int i = 0; i < str.length(); i++) {3 System.out.print(str.charAt(i));4 }5 System.out.println();6 }
這是什么意思呢?加上synchronized關(guān)鍵字后,比如A線程執(zhí)行out方法就相當(dāng)于拿到了一把鎖,只有獲取這個(gè)鎖才能執(zhí)行此方法,如果在A線程執(zhí)行out方法過(guò)程中B線程也想插一腳進(jìn)來(lái)執(zhí)行out方法,對(duì)不起此時(shí)這是不能夠的,因?yàn)榇藭r(shí)鎖在A線程手里,B線程無(wú)權(quán)拿到這把鎖,只有等到A線程執(zhí)行完后放棄鎖,B線程才能拿到鎖執(zhí)行out方法。
為out方法加上synchronized后其就變成了同步方法,普通同步方法的鎖是this,也就是當(dāng)前對(duì)象,比如demo中,外部要想調(diào)用out方法就必須創(chuàng)建OutPutStr類實(shí)例對(duì)象o,此時(shí)out同步方法的鎖就是這個(gè)o。
四、同步代碼塊解決上述問(wèn)題
我們也可以利用同步代碼塊解決上述問(wèn)題,修改out方法如下:
1 public voidout(String str) {2 synchronized (this) {3 for (int i = 0; i < str.length(); i++) {4 System.out.print(str.charAt(i));5 }6 System.out.println();7 }8 }
同步代碼塊寫法:synchronized(obj){},其中obj為鎖對(duì)象,此處我們傳入this,同樣方法的鎖也為當(dāng)前對(duì)象,如果此處我們傳入str,那么這里的鎖就是str對(duì)象了。
為了說(shuō)明不同鎖帶來(lái)的影響我們修改OutPutStr代碼如下:
1 public classOutPutStr {2
3 public synchronized voidout(String str) {4 for (int i = 0; i < str.length(); i++) {5 System.out.print(str.charAt(i));6 }7 System.out.println();8 }9
10 public voidout1(String str) {11
12 synchronized(str) {13 for (int i = 0; i < str.length(); i++) {14 System.out.print(str.charAt(i));15 }16 System.out.println();17 }18 }19 }
很簡(jiǎn)單我們就是加入了一個(gè)out1方法,out方法用同步函數(shù)保證同步,out1用同步代碼塊保證代碼塊,但是鎖我們用的是str。
main代碼:
1 public static voidmain(String[] args) {2 //3 final OutPutStr o = newOutPutStr();4 new Thread(newRunnable() {5
6 @Override7 public voidrun() {8 //9 while(true){10 o.out("111111111111");11 }12 }13 }).start();14 new Thread(newRunnable() {15
16 @Override17 public voidrun() {18 //19 while(true){20 o.out1("222222222222");21 }22 }23 }).start();24 }
也沒(méi)什么,就是其中一個(gè)線程調(diào)用out方法,另一個(gè)調(diào)用out1方法,運(yùn)行程序:
111111111111222
222222222222
111111111111222222222222
222222222222
看到了吧,打印信息又出問(wèn)題了,就是因?yàn)閛ut與out1方法的鎖不一樣導(dǎo)致的,線程A調(diào)用out方法拿到this這把鎖,線程B調(diào)用out1拿到str這把鎖,二者互不影響,解決辦法也很簡(jiǎn)單,修改out1方法如下即可:
1 public voidout1(String str) {2
3 synchronized (this) {4 for (int i = 0; i < str.length(); i++) {5 System.out.print(str.charAt(i));6 }7 System.out.println();8 }9 }
五、靜態(tài)函數(shù)的同步問(wèn)題
我們繼續(xù)修改OutPutStr類,加入out2方法:
1 public classOutPutStr {2
3 public synchronized voidout(String str) {4 for (int i = 0; i < str.length(); i++) {5 System.out.print(str.charAt(i));6 }7 System.out.println();8 }9
10 public voidout1(String str) {11
12 synchronized (this) {13 for (int i = 0; i < str.length(); i++) {14 System.out.print(str.charAt(i));15 }16 System.out.println();17 }18 }19
20 public synchronized static voidout2(String str) {21
22 for (int i = 0; i < str.length(); i++) {23 System.out.print(str.charAt(i));24 }25 System.out.println();26 }27 }
main中兩個(gè)子線程分別調(diào)用out1,ou2打印信息,運(yùn)行程序打印信息如下;
1 222222222222
2 222222222222
3 222222222111111111111
4 111111111111
咦?又出錯(cuò)了,out2與out方法唯一不同就是out2就是靜態(tài)方法啊,不是說(shuō)同步方法鎖是this嗎,是啊,沒(méi)錯(cuò),但是靜態(tài)方法沒(méi)有對(duì)應(yīng)類的實(shí)例對(duì)象依然可以調(diào)用,那其鎖是誰(shuí)呢?顯然靜態(tài)方法鎖不是this,這里就直說(shuō)了,是類的字節(jié)碼對(duì)象,類的字節(jié)碼對(duì)象是優(yōu)先于類實(shí)例對(duì)象存在的。
將ou1方法改為如下:
1 public voidout1(String str) {2
3 synchronized (OutPutStr.class) {4 for (int i = 0; i < str.length(); i++) {5 System.out.print(str.charAt(i));6 }7 System.out.println();8 }9 }
再次運(yùn)行程序,就會(huì)發(fā)現(xiàn)信息能正常打印了。
六、synchronized同步方式總結(jié)
到此我們就該小小的總結(jié)一下了,普通同步函數(shù)的鎖是this,當(dāng)前類實(shí)例對(duì)象,同步代碼塊鎖可以自己定義,靜態(tài)同步函數(shù)的鎖是類的字節(jié)碼文件??偨Y(jié)完畢,就是這么簡(jiǎn)單。說(shuō)了一大堆理解這一句就夠了。
七、JDK1.5中Lock鎖機(jī)制解決線程同步
大家是不是覺(jué)得上面說(shuō)的鎖這個(gè)玩意咋這么抽象,看不見(jiàn),摸不著的。從JDK1.5起我們就可以根據(jù)需要顯性的獲取鎖以及釋放鎖了,這樣也更加符合面向?qū)ο笤瓌t。
Lock接口的實(shí)現(xiàn)子類之一ReentrantLock,翻譯過(guò)來(lái)就是重入鎖,就是支持重新進(jìn)入的鎖,該鎖能夠支持一個(gè)線程對(duì)資源的重復(fù)加鎖,也就是說(shuō)在調(diào)用lock()方法時(shí),已經(jīng)獲取到鎖的線程,能夠再次調(diào)用lock()方法獲取鎖而不被阻塞,同時(shí)還支持獲取鎖的公平性和非公平性,所謂公平性就是多個(gè)線程發(fā)起lock()請(qǐng)求,先發(fā)起的線程優(yōu)先獲取執(zhí)行權(quán),非公平性就是獲取鎖與是否優(yōu)先發(fā)起lock()操作無(wú)關(guān)。默認(rèn)情況下是不公平的鎖,為什么要這樣設(shè)計(jì)呢?現(xiàn)實(shí)生活中我們都希望公平的啊?我們想一下,現(xiàn)實(shí)生活中要保證公平就必須額外開(kāi)銷,比如地鐵站保證有序公平進(jìn)站就必須配備額外人員維持秩序,程序中也是一樣保證公平就必須需要額外開(kāi)銷,這樣性能就下降了,所以公平與性能是有一定矛盾的,除非公平策略對(duì)你的程序很重要,比如必須按照順序執(zhí)行線程,否則還是使用不公平鎖為好。
接下來(lái)我們修改OutPutStr類,添加out3方法:
1 //true表示公平鎖,false非公平鎖
2 private Lock lock = new ReentrantLock();3
4 public voidout3(String str) {5
6 lock.lock();//如果有其它線程已經(jīng)獲取鎖,那么當(dāng)前線程在此等待直到其它線程釋放鎖。
7 try{8 for (int i = 0; i < str.length(); i++) {9 System.out.print(str.charAt(i));10 }11 System.out.println();12 } finally{13 lock.unlock();//釋放鎖資源,之所以加入try{}finally{}代碼塊,14 //是為了保證鎖資源的釋放,如果代碼發(fā)生異常也可以保證鎖資源的釋放,15 //否則其它線程無(wú)法拿到鎖資源執(zhí)行業(yè)務(wù)邏輯,永遠(yuǎn)處于等待狀態(tài)。
16 }17 }
關(guān)鍵注釋都在代碼中有所體現(xiàn)了,使用起來(lái)也很簡(jiǎn)單。
八、Lock與synchronized同步方式優(yōu)缺點(diǎn)
Lock 的鎖定是通過(guò)代碼實(shí)現(xiàn)的,而 synchronized 是在 JVM 層面上實(shí)現(xiàn)的(所有對(duì)象都自動(dòng)含有單一的鎖。JVM負(fù)責(zé)跟蹤對(duì)象被加鎖的次數(shù)。如果一個(gè)對(duì)象被解鎖,其計(jì)數(shù)變?yōu)?。在線程第一次給對(duì)象加鎖的時(shí)候,計(jì)數(shù)變?yōu)?。每當(dāng)這個(gè)相同的線程在此對(duì)象上獲得鎖時(shí),計(jì)數(shù)會(huì)遞增。只有首先獲得鎖的線程才能繼續(xù)獲取該對(duì)象上的多個(gè)鎖。每當(dāng)線程離開(kāi)一個(gè)synchronized方法,計(jì)數(shù)遞減,當(dāng)計(jì)數(shù)為0的時(shí)候,鎖被完全釋放,此時(shí)別的線程就可以使用此資源)。
synchronized 在鎖定時(shí)如果方法塊拋出異常,JVM 會(huì)自動(dòng)將鎖釋放掉,不會(huì)因?yàn)槌隽水惓](méi)有釋放鎖造成線程死鎖。但是 Lock 的話就享受不到 JVM 帶來(lái)自動(dòng)的功能,出現(xiàn)異常時(shí)必須在 finally 將鎖釋放掉,否則將會(huì)引起死鎖。
在資源競(jìng)爭(zhēng)不是很激烈的情況下,偶爾會(huì)有同步的情形下,synchronized是很合適的。原因在于,編譯程序通常會(huì)盡可能的進(jìn)行優(yōu)化synchronize,另外可讀性非常好。在資源競(jìng)爭(zhēng)激烈情況下,Lock同步機(jī)制性能會(huì)更好一些。
關(guān)于線程同步問(wèn)題到這里就結(jié)束了,java多線程文章只是本人工作以來(lái)的一次梳理,都比較基礎(chǔ),但是卻很重要的,最近招人面試的最大體會(huì)就是都喜歡那些所謂時(shí)髦的技術(shù)一問(wèn)基礎(chǔ)說(shuō)的亂七八糟,浪費(fèi)彼此的時(shí)間。好啦,吐槽了幾句,本文到此為止,很基礎(chǔ)的玩意,希望對(duì)你有用。
聲明:文章將會(huì)陸續(xù)搬遷到個(gè)人公眾號(hào),以后文章也會(huì)第一時(shí)間發(fā)布到個(gè)人公眾號(hào),及時(shí)獲取文章內(nèi)容請(qǐng)關(guān)注公眾號(hào)
總結(jié)
以上是生活随笔為你收集整理的java 多线程同步问题_Java多线程同步问题:一个小Demo完全搞懂的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 高速路上一觉醒来车在冒烟无人驾驶:副驾小
- 下一篇: 约2000元 vivo发布Y100新机