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