Java多線程同步問題:一個小Demo徹底搞懂

版權聲明:本文出自汪磊的博客,轉載請務必註明出處。java

Java線程系列文章只是本身知識的總結梳理,都是最基礎的玩意,已經掌握熟練的能夠繞過。面試

1、一個簡單的Demo引起的血案多線程

關於線程同步問題咱們從一個簡單的Demo現象提及。Demo特別簡單就是開啓兩個線程打印字符串信息。ide

OutPutStr類源碼:函數

1 public class OutPutStr { 2 
3     public void out(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 void main(String[] args) {
 2         //
 3         final OutPutStr o = new OutPutStr();
 4         new Thread(new Runnable() {
 5 
 6             @Override
 7             public void run() {
 8                 //
 9                 while(true){
10                     o.out("111111111111");
11                 }
12             }
13         }).start();
14         new Thread(new Runnable() {
15 
16             @Override
17             public void run() {
18                 //
19                 while(true){
20                     o.out("222222222222");
21                 }
22             }
23         }).start();
24 }

也很簡單,就是開啓兩個線程分別調用OutPutStr中out方法不停打印字符串信息,運行程序打印信息以下:this

1 222222222222
2 222222222222
3 22222222222111111111
4 2
5 111111111111
6 111111111111
7 1111222222222211111111
8 111111111111

咦?和咱們想的不同啊,怎麼還會打印出22222222222111111111這樣子的信息,這是怎麼回事呢?spa

2、緣由解析線程

咱們知道線程的執行是CPU隨機調度的,好比咱們開啓10個線程,這10個線程並非同時執行的,而是CPU快速的在這10個線程之間切換執行,因爲切換速度極快使咱們感受同時執行罷了。發生上面問題的本質就是CPU對線程執行的隨機調度,好比A線程此時正在打印信息還沒打印完畢此時CPU切換到B線程執行了,B線程執行完了又切換回A線程執行就會致使上面現象發生。

線程同步問題每每發生在多個線程調用同一方法或者操做同一變量,可是咱們要知道其本質就是CPU對線程的隨機調度,CPU沒法保證一個線程執行完其邏輯纔去調用另外一個線程執行。

3、同步方法解決上述問題

既然知道了問題發生的緣由,記下來咱們就要想辦法解決問題啊,解決的思路就是保證一個線程在調用out方法的時候若是沒執行完那麼另外一個不能執行此方法,換句話說就是隻能等待別的線程執行完畢才能執行。

針對線程同步問題java早就有解決方法了,最簡單的就是給方法加上synchronized關鍵字,以下:

1 public synchronized void out(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線程執行out方法就至關於拿到了一把鎖,只有獲取這個鎖才能執行此方法,若是在A線程執行out方法過程當中B線程也想插一腳進來執行out方法,對不起此時這是不可以的,由於此時鎖在A線程手裏,B線程無權拿到這把鎖,只有等到A線程執行完後放棄鎖,B線程才能拿到鎖執行out方法。

爲out方法加上synchronized後其就變成了同步方法,普通同步方法的鎖是this,也就是當前對象,好比demo中,外部要想調用out方法就必須建立OutPutStr類實例對象o,此時out同步方法的鎖就是這個o。

4、同步代碼塊解決上述問題

咱們也能夠利用同步代碼塊解決上述問題,修改out方法以下:

1 public void out(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 class OutPutStr {
 2 
 3     public synchronized void out(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 void out1(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 void main(String[] args) {
 2         //
 3         final OutPutStr o = new OutPutStr();
 4         new Thread(new Runnable() {
 5 
 6             @Override
 7             public void run() {
 8                 //
 9                 while(true){
10                     o.out("111111111111");
11                 }
12             }
13         }).start();
14         new Thread(new Runnable() {
15 
16             @Override
17             public void run() {
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 void out1(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 }

5、靜態函數的同步問題

咱們繼續修改OutPutStr類,加入out2方法:

 1 public class OutPutStr {
 2 
 3     public synchronized void out(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 void out1(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 void out2(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就是靜態方法啊,不是說同步方法鎖是this嗎,是啊,沒錯,可是靜態方法沒有對應類的實例對象依然能夠調用,那其鎖是誰呢?顯然靜態方法鎖不是this,這裏就直說了,是類的字節碼對象,類的字節碼對象是優先於類實例對象存在的。

將ou1方法改成以下:

1 public void out1(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 }

再次運行程序,就會發現信息能正常打印了。

6、synchronized同步方式總結

到此咱們就該小小的總結一下了,普通同步函數的鎖是this,當前類實例對象,同步代碼塊鎖能夠本身定義,靜態同步函數的鎖是類的字節碼文件。總結完畢,就是這麼簡單。說了一大堆理解這一句就夠了。

7、JDK1.5中Lock鎖機制解決線程同步

你們是否是以爲上面說的鎖這個玩意咋這麼抽象,看不見,摸不着的。從JDK1.5起咱們就能夠根據須要顯性的獲取鎖以及釋放鎖了,這樣也更加符合面向對象原則。

Lock接口的實現子類之一ReentrantLock,翻譯過來就是重入鎖,就是支持從新進入的鎖,該鎖可以支持一個線程對資源的重複加鎖,也就是說在調用lock()方法時,已經獲取到鎖的線程,可以再次調用lock()方法獲取鎖而不被阻塞,同時還支持獲取鎖的公平性和非公平性,所謂公平性就是多個線程發起lock()請求,先發起的線程優先獲取執行權,非公平性就是獲取鎖與是否優先發起lock()操做無關。默認狀況下是不公平的鎖,爲何要這樣設計呢?現實生活中咱們都但願公平的啊?咱們想一下,現實生活中要保證公平就必須額外開銷,好比地鐵站保證有序公平進站就必須配備額外人員維持秩序,程序中也是同樣保證公平就必須須要額外開銷,這樣性能就降低了,因此公平與性能是有必定矛盾的,除非公平策略對你的程序很重要,好比必須按照順序執行線程,不然仍是使用不公平鎖爲好。

接下來咱們修改OutPutStr類,添加out3方法:

 1 //true表示公平鎖,false非公平鎖
 2     private Lock lock = new ReentrantLock();
 3     
 4     public void out3(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             //是爲了保證鎖資源的釋放,若是代碼發生異常也能夠保證鎖資源的釋放,
15             //不然其它線程沒法拿到鎖資源執行業務邏輯,永遠處於等待狀態。
16         }
17     }

關鍵註釋都在代碼中有所體現了,使用起來也很簡單。

8、Lock與synchronized同步方式優缺點

Lock 的鎖定是經過代碼實現的,而 synchronized 是在 JVM 層面上實現的(全部對象都自動含有單一的鎖。JVM負責跟蹤對象被加鎖的次數。若是一個對象被解鎖,其計數變爲0。在線程第一次給對象加鎖的時候,計數變爲1。每當這個相同的線程在此對象上得到鎖時,計數會遞增。只有首先得到鎖的線程才能繼續獲取該對象上的多個鎖。每當線程離開一個synchronized方法,計數遞減,當計數爲0的時候,鎖被徹底釋放,此時別的線程就可使用此資源)。

synchronized 在鎖定時若是方法塊拋出異常,JVM 會自動將鎖釋放掉,不會由於出了異常沒有釋放鎖形成線程死鎖。可是 Lock 的話就享受不到 JVM 帶來自動的功能,出現異常時必須在 finally 將鎖釋放掉,不然將會引發死鎖。

在資源競爭不是很激烈的狀況下,偶爾會有同步的情形下,synchronized是很合適的。緣由在於,編譯程序一般會盡量的進行優化synchronize,另外可讀性很是好。在資源競爭激烈狀況下,Lock同步機制性能會更好一些。

 

關於線程同步問題到這裏就結束了,java多線程文章只是本人工做以來的一次梳理,都比較基礎,可是卻很重要的,最近招人面試的最大致會就是都喜歡那些所謂時髦的技術一問基礎說的亂七八糟,浪費彼此的時間。好啦,吐槽了幾句,本文到此爲止,很基礎的玩意,但願對你有用。

聲明:文章將會陸續搬遷到我的公衆號,之後文章也會第一時間發佈到我的公衆號,及時獲取文章內容請關注公衆號

相關文章
相關標籤/搜索