關於Java多執行緒的執行緒同步和執行緒通訊的一些小問題(順便分享幾篇高質量的博文)

JNovice發表於2018-08-05

Java多執行緒的執行緒同步和執行緒通訊的一些小問題(順便分享幾篇質量高的博文)


  • 前言:在學習多執行緒時,遇到了一些問題,這裡我將這些問題都分享出來,同時也分享了幾篇其他部落格主的部落格,並且將我個人的理解也分享給大家,本人的水平有限,如果我的分析或者結論有錯誤希望大家一定要幫我指出來,我好能改正和提高

一、對於執行緒同步和同步鎖的理解(注:分享了三篇高質量的部落格)


以下我精心的挑選了幾篇博文,分別是關於對執行緒同步的理解和如何選擇執行緒鎖以及瞭解執行緒鎖的作用範圍。

<一>執行緒同步鎖的選擇

1. 這裡我推薦下陸先生的Java程式碼質量改進之:同步物件的選擇這篇博文。

2. 以上推薦的博文是以賣火車票為例,引出了非同步會導致的錯誤以及同步鎖(監視器)應該如果選擇,應該能夠幫助大家理解同步鎖。


<二>執行緒同伴鎖用法及同步鎖的作用範圍

1. 這裡我推薦下Java中synchronized同步鎖用法及作用範圍這篇博文。

2. 以上的博文將靜態鎖(位元組碼檔案鎖)和非靜態鎖(this)進行了對比,以及將執行緒非同步和執行緒同步下進行了對比,對大家瞭解執行緒鎖的用法和作用範圍有很大的幫助。

 

<三>對執行緒同步的理解

1. 這裡我推薦下java中執行緒同步的理解(非常通俗易懂)這篇博文。

2. 以上推薦的博文以非常通俗易懂的觀點解釋了到時什麼同步,將同步理解成了執行緒同步就是執行緒排隊,而且舉了一些日常生活中的例子來讓大家理解到底什麼是同伴。

 

<四>同步的作用場景

1. 並不是說同步在什麼情況下都是好的,因為執行緒的同步會帶來較低效率,因為執行緒同步就代表著執行緒要排隊,即執行緒同步鎖會帶來的同步阻塞狀態。

2. 因為CPU是隨意切換執行緒的,當我們想讓當前執行緒執行之後CPU不隨意切換到其他執行緒,或者我們想要讓某個執行緒的程式碼能夠在完全執行之前不會被搶奪執行權,不會導致從而無法連續執行,那麼我們就需要執行緒的幫助。

 

二、執行緒同步和執行緒通訊的幾個小細節


以下是我在學習執行緒同步時,遇到的小問題和我的小感悟

 <一>執行緒sleep方法的基本用法和注意細節

    1.sleep方法的基本用法

Thread.sleep(long millis),傳入毫秒數(1秒 = 1000毫秒),在指定的毫秒數內讓當前正在執行的執行緒休眠(暫停執行),此操作受到系統計時器和排程程式精度和準確性的影響。該執行緒不丟失任何監視器的所屬權。(~注:Java技術文件的意思就是該執行緒休眠指定的毫秒數,而且休眠狀態暫時失去CPU執行權,而且執行緒醒來後,該執行緒不會釋放鎖。)

 1 /**
 2  * 
 3  * Thread.sleep的計時器用法
 4  *
 5  */
 6 public class ThreadSleepTest {
 7 
 8     public static void main(String[] args) {
 9         new Thread() {
10             @Override
11             public void run() {
12                 int timeCount = 10;
13                 while (timeCount >= 0) {
14                     if (timeCount == 0) {
15                         System.out.println("新年快樂!~");
16                         break;
17                     }
18                     System.out.println("還剩" + timeCount-- + "秒");
19                     try {
20                         Thread.sleep(1000);
21                     } catch (InterruptedException e) {
22                         e.printStackTrace();
23                     }
24                 }
25             }
26         }.start();
27     }
28 
29 }

 

    2.sleep方法使用的位置選擇

我在使用sleep方法時發現,當sleep的位置不一致所放的位置不同時,執行緒所執行的結果也是大不相同的,以下的程式碼是為了舉例子,並不是說這個同步程式碼塊就是應這樣寫(其實這段程式碼這麼寫是有很大的問題的,因為同步資源的選擇不準確),至於同步資源的選擇我在第二個大問題會講到。

      • A 以下的程式碼是sleep方法出現在了售票的程式碼塊之前,這時出現了負票。(可能時間上也會導致差異,但是這裡先不考慮時間時間因素,時間因素等下講。)
 1 package javase.week4;
 2 
 3 public class SellTrainTickets {
 4 
 5     public static void main(String[] args) {
 6         new MyThread("視窗1").start();
 7         new MyThread("視窗2").start();
 8         new MyThread("視窗3").start();
 9         new MyThread("視窗4").start();
10     }
11 
12 }
13 
14 class MyThread extends Thread {
15     
16     static int tickets = 100;
17 
18     public MyThread(String name) {
19         super(name);
20     }
21 
22     @Override
23     public void run() {
24         while (tickets > 0) {//假設這已經減到了1  1>0 然後視窗1 視窗2 視窗3 視窗4 都進入迴圈
25             try {
26                 Thread.sleep(20);
27                 
28             } catch (InterruptedException e) {
29                 e.printStackTrace();
30             }
31             synchronized (MyThread.class) {
32                 System.out.println(getName() + "賣出了第" + tickets-- + "張票!");//然後0 -1 -2 -3,這時就出現了負票
33             }
34         }
35     }
36 }

 

      • B 以下的程式碼是sleep方法出現在了售票的程式碼塊之後,這裡沒有出現負票了,在票數100的情況下,而且時間是20毫秒的情況下,該段程式碼正好保證了時間點上的合理性,但是相同情況下sleep方法出現在輸出售票之前的程式碼就會出現錯誤,即使改變時間和票數其sleep方法出現的位置錯誤,還是會導致了在票數為負的情況。(其實如果票數更改或者時間的改變也可能導致sleep方法出現在售票程式碼塊之後的情況下負票的出現)
 1 public class SellTrainTickets {
 2 
 3     public static void main(String[] args) {
 4         new MyThread("視窗1").start();
 5         new MyThread("視窗2").start();
 6         new MyThread("視窗3").start();
 7         new MyThread("視窗4").start();
 8     }
 9 
10 }
11 
12 class MyThread extends Thread {
13     
14     static int tickets = 100;
15 
16     public MyThread(String name) {
17         super(name);
18     }
19 
20     @Override
21     public void run() {
22         while (tickets > 0) {//假設這已經減到了0  5>0 然後視窗1 視窗2 視窗3 視窗4 都進入迴圈
23             synchronized (MyThread.class) {
24                 System.out.println(getName() + "賣出了第" + tickets-- + "張票!");//然後 3 2 1 0
25             }
26             try {
27                 Thread.sleep(20);//此時票數等於0,這時視窗1 視窗2 視窗3 視窗4 都處於休眠,然後如果這裡的時間合理的話,再次判斷的話,正好在等於0的時候,都沒有執行緒再次進入迴圈,也就不會出現負票了
28                 
29             } catch (InterruptedException e) {
30                 e.printStackTrace();
31             }
32         }
33     }
34 }

 

      • C 總結下其實sleep方法出現的位置可能會影響到執行緒的結果,但是其實一般情況下是不會這麼樣去使用的,這裡只是為了演示下sleep方法在位置不同的情況下出現的不同的結果,目的是為了讓大家注意程式設計的細節。

 

     3.sleep方法的傳入引數的選擇

sleep方法的傳入的毫秒數對於執行緒的執行結果是有較大的影響的,最直接簡單的影響就是讓執行延遲了,但是除了這個以外其實也讓執行緒的執行結果發生了變化,順便分享一篇一篇高質量的博文Sleep(0)的妙用

      • A  當傳入的引數為100時,以下是程式碼演示和執行結果,幾乎每一個視窗(執行緒)都可以搶奪到執行權,而且比較分散。
 1 public class TicketsThreadTest {
 2 
 3     public static void main(String[] args) {
 4         new TicketThread("視窗1").start();
 5         new TicketThread("視窗2").start();
 6         new TicketThread("視窗3").start();
 7         new TicketThread("視窗4").start();
 8     }
 9 
10 }
11 
12 class TicketThread extends Thread {
13 
14     public TicketThread(String name) {
15         super(name);
16     }
17 
18     private static int ticket = 100;
19 
20     public void run() {
21         while (true) {
22             synchronized (TicketThread.class) {
23                 if (ticket <= 0) {
24                     break;
25                 }
26                 System.out.println(getName() + "賣出了第" + ticket-- + "張票!");
27                 try {
28                     Thread.sleep(100);
29                 } catch (InterruptedException e) {
30                     e.printStackTrace();
31                 }
32             }
33         }
34     }
35 }

 

      • B 當傳入的引數為1時,以下是程式碼演示和執行結果,這次執行的效果就不是很好,並不是每一個執行緒都能很好的執行到,或者執行得不是很分散。
 1 public class TicketsThreadTest {
 2 
 3     public static void main(String[] args) {
 4         new TicketThread("視窗1").start();
 5         new TicketThread("視窗2").start();
 6         new TicketThread("視窗3").start();
 7         new TicketThread("視窗4").start();
 8     }
 9 
10 }
11 
12 class TicketThread extends Thread {
13 
14     public TicketThread(String name) {
15         super(name);
16     }
17 
18     private static int ticket = 100;
19 
20     public void run() {
21         while (true) {
22             synchronized (TicketThread.class) {
23                 if (ticket <= 0) {
24                     break;
25                 }
26                 System.out.println(getName() + "賣出了第" + ticket-- + "張票!");
27                 try {
28                     Thread.sleep(1);
29                 } catch (InterruptedException e) {
30                     e.printStackTrace();
31                 }
32             }
33         }
34     }
35 }

 

 

      • C 總結以下時間引數對執行緒的結果的影響,以賣火車票為例,當我們在sleep方法中輸入不同的引數,那麼執行緒的執行結果就發生了變化,因為當我們給定的休眠期長了,那麼執行緒的搶奪CPU執行權的速度就放緩了,此時執行的結果就變得比較分散,如果幾乎沒有休眠期那麼搶到執行權的視窗(執行緒)可能還是處於領先優勢,sleep方法其實讓處於優先地位的暫時休眠讓出了CPU執行權,然後sleep醒來又處於就緒狀態來搶奪資源。這樣不會讓其他執行緒變成無法執行的尷尬境遇。當然後續可以使用wait和notify以及notifyAll的方法,讓執行緒進行有規律地交替執行。

 

<二>明確需要同步的共享資源

       如果這裡同步的是程式碼塊不是程式碼方法,那麼這裡需要對要同步的共享資源的選擇要準確,如果選擇得不準確會導致結果不理想。

      • A 以下程式碼表示選擇的共享程式碼塊為售票的單個輸出語句,此時可以看出結果,結果出現了負票
 1 public class TicketsThreadTest {
 2 
 3     public static void main(String[] args) {
 4         new TicketThread("視窗1").start();
 5         new TicketThread("視窗2").start();
 6         new TicketThread("視窗3").start();
 7         new TicketThread("視窗4").start();
 8     }
 9 
10 }
11 
12 class TicketThread extends Thread {
13 
14     public TicketThread(String name) {
15         super(name);
16     }
17 
18     private static int ticket = 100;
19 
20     public void run() {
21         while (true) {
22                 if (ticket <= 0) {
23                     break;
24                 }
25                 synchronized (TicketThread.class) {
26                 System.out.println(getName() + "賣出了第" + ticket-- + "張票!");
27                 }
28                 try {
29                     Thread.sleep(0);
30                 } catch (InterruptedException e) {
31                     e.printStackTrace();
32             }
33         }
34     }
35 }

 

      • B 以下程式碼表示選擇的共享程式碼塊是while迴圈的整個程式碼塊,此時可以看出結果,結果沒有出現負票,而且經過了多次嘗試也沒有出現
 1 public class TicketsThreadTest {
 2 
 3     public static void main(String[] args) {
 4         new TicketThread("視窗1").start();
 5         new TicketThread("視窗2").start();
 6         new TicketThread("視窗3").start();
 7         new TicketThread("視窗4").start();
 8     }
 9 
10 }
11 
12 class TicketThread extends Thread {
13 
14     public TicketThread(String name) {
15         super(name);
16     }
17 
18     private static int ticket = 100;
19 
20     public void run() {
21         while (true) {
22             synchronized (TicketThread.class) {
23                 if (ticket <= 0) {
24                     break;
25                 }
26                 System.out.println(getName() + "賣出了第" + ticket-- + "張票!");
27                 try {
28                     Thread.sleep(0);
29                 } catch (InterruptedException e) {
30                     e.printStackTrace();
31                 }
32             }
33         }
34     }
35 }

 

      • C 總結在選擇需要同步的程式碼塊是一定要注意哪些程式碼塊(資源是需要共享的),這裡就要判斷下這些程式碼是否是需要共享,將需要共享的資源用synchronized程式碼塊包起來

 

<三>執行緒通訊之while和if的選擇

      • A 以下的程式碼使用的是if選擇結構進行執行緒通訊之間的判斷,可以發現三個執行緒之間沒有有規律地交替進行。
  1 package javase.week4;
  2 
  3 /**
  4  * 
  5  * 三個執行緒之間的通訊使用if選擇語句
  6  *
  7  */
  8 public class ComunicatedThreadTest {
  9     public static void main(String[] args) {
 10         Printer1121 p = new Printer1121();
 11         new Thread() {
 12             @Override
 13             public void run() {
 14                 while (true) {
 15                     try {
 16                         p.print1();
 17                     } catch (Exception e) {
 18                         e.printStackTrace();
 19                     }
 20                 }
 21             }
 22         }.start();
 23         new Thread() {
 24             @Override
 25             public void run() {
 26                 while (true) {
 27                     try {
 28                         p.print2();
 29                     } catch (Exception e) {
 30                         e.printStackTrace();
 31                     }
 32                 }
 33             }
 34         }.start();
 35 
 36         new Thread() {
 37             @Override
 38             public void run() {
 39                 while (true) {
 40                     try {
 41                         p.print3();
 42                     } catch (Exception e) {
 43                         e.printStackTrace();
 44                     }
 45                 }
 46             }
 47         }.start();
 48     }
 49 
 50 }
 51 
 52 class Printer1121 {
 53     private int flag = 1;
 54 
 55     public void print1() throws Exception {
 56         synchronized (this) {
 57             if (flag != 1) {
 58                 this.wait();
 59             }
 60             Thread.sleep(100);
 61             System.out.print(1);
 62             System.out.print(2);
 63             System.out.print(3);
 64             System.out.print(4);
 65             System.out.print(5);
 66             System.out.println();
 67             flag = 2;
 68             this.notifyAll();
 69         }
 70     }
 71 
 72     public void print2() throws Exception {
 73         synchronized (this) {
 74             if (flag != 2) {
 75                 this.wait();
 76             }
 77             Thread.sleep(100);
 78             System.out.print("a");
 79             System.out.print("b");
 80             System.out.print("c");
 81             System.out.print("d");
 82             System.out.print("e");
 83             System.out.println();
 84             flag = 3;
 85             this.notifyAll();
 86         }
 87     }
 88 
 89     public void print3() throws Exception {
 90         synchronized (this) {
 91             if (flag != 3) {
 92                 this.wait();
 93             }
 94             Thread.sleep(100);
 95             System.out.print("A");
 96             System.out.print("B");
 97             System.out.print("C");
 98             System.out.print("D");
 99             System.out.print("E");
100             System.out.println();
101             flag = 1;
102             this.notifyAll();
103         }
104     }
105 }

 

      • B 以下是用while對通訊條件進行迴圈判斷的,可以發現三個執行緒是有規律地迴圈進行執行的。
  1 package javase.week4;
  2 /**
  3  * 
  4  * 三個執行緒之間的通訊使用while迴圈判斷語句
  5  *
  6  */
  7 public class ComunicatedThreadTest {
  8     public static void main(String[] args) {
  9         Printer1121 p = new Printer1121();
 10         new Thread() {
 11             @Override
 12             public void run() {
 13                 while (true) {
 14                     try {
 15                         p.print1();
 16                     } catch (Exception e) {
 17                         e.printStackTrace();
 18                     }
 19                 }
 20             }
 21         }.start();
 22         new Thread() {
 23             @Override
 24             public void run() {
 25                 while (true) {
 26                     try {
 27                         p.print2();
 28                     } catch (Exception e) {
 29                         e.printStackTrace();
 30                     }
 31                 }
 32             }
 33         }.start();
 34 
 35         new Thread() {
 36             @Override
 37             public void run() {
 38                 while (true) {
 39                     try {
 40                         p.print3();
 41                     } catch (Exception e) {
 42                         e.printStackTrace();
 43                     }
 44                 }
 45             }
 46         }.start();
 47     }
 48 
 49 }
 50 
 51 class Printer1121 {
 52     private int flag = 1;
 53 
 54     public void print1() throws Exception {
 55         synchronized (this) {
 56             while (flag != 1) {
 57                 this.wait();
 58             }
 59             Thread.sleep(100);
 60             System.out.print(1);
 61             System.out.print(2);
 62             System.out.print(3);
 63             System.out.print(4);
 64             System.out.print(5);
 65             System.out.println();
 66             flag = 2;
 67             this.notifyAll();
 68         } 
 69     }
 70 
 71     public void print2() throws Exception {
 72         synchronized (this) {
 73             while (flag != 2) {
 74                 this.wait();
 75             }
 76             Thread.sleep(100);
 77             System.out.print("a");
 78             System.out.print("b");
 79             System.out.print("c");
 80             System.out.print("d");
 81             System.out.print("e");
 82             System.out.println();
 83             flag = 3;
 84             this.notifyAll();
 85         }
 86     }
 87 
 88     public void print3() throws Exception {
 89         synchronized (this) {
 90             while (flag != 3) {
 91                 this.wait();
 92             }
 93             Thread.sleep(100);
 94             System.out.print("A");
 95             System.out.print("B");
 96             System.out.print("C");
 97             System.out.print("D");
 98             System.out.print("E");
 99             System.out.println();
100             flag = 1;
101             this.notifyAll();
102         }
103     }
104 }

      • C 這裡進行下原因分析,為什麼會出現這樣的情況?首先wait方法在同步程式碼塊裡被呼叫了,那麼此時呼叫者直接在wait處等待了,然後等待下次被notify或者notifyAll喚醒。而if選擇結構在判斷一次之後就順序執行了,當執行緒被喚醒時,我們希望的是再次判斷一次條件看能夠繼續進行,但是if無法做到,因為上次已經判斷正確了,它只會向下繼續執行此時就會又出現隨意無規律交替執行,但是while是迴圈判斷,即使判斷過一次了,但是每次執行完它會再次判斷,這時就會讓三個執行緒的執行結果有規律了。

 

相關文章