Java多線程——線程之間的協做

Java多線程——線程之間的協做

摘要:本文主要學習多線程之間是如何協做的,以及如何使用wait()方法與notify()/notifyAll()方法。html

部份內容來自如下博客:java

https://www.cnblogs.com/hapjin/p/5492645.html安全

https://www.cnblogs.com/sharpxiajun/p/2295677.html多線程

https://www.cnblogs.com/90zeng/p/java_multithread_2.html併發

爲何線程之間須要進行協做

在多線程併發的狀況下,若是都對共享資源進行操做,那麼會致使線程安全問題,因此咱們使用線程的同步機制來保證多線程環境下程序的安全性,可是使用同步機制只能保證線程安全,並不能在兩個線程或者多個線程之間自由切換,線程的切換徹底受CPU的影響。ide

若是使用同步機制讓兩個線程交替打印1到10的數字,代碼以下:學習

 1 public class Demo {
 2     public static void main(String[] args) {
 3         DemoThread demoThread = new DemoThread();
 4         Thread a = new Thread(demoThread, "線程A");
 5         Thread b = new Thread(demoThread, "線程B");
 6         a.start();
 7         b.start();
 8     }
 9 }
10 
11 class DemoThread implements Runnable {
12     private int num = 1;
13 
14     @Override
15     public void run() {
16         while (num <= 10) {
17             synchronized (DemoThread.class) {
18                 if (num <= 10) {
19                     System.out.println(Thread.currentThread().getName() + " >>> " + num++);
20                 }
21             }
22         }
23     }
24 }

運行結果以下:this

 1 線程A >>> 1
 2 線程A >>> 2
 3 線程A >>> 3
 4 線程A >>> 4
 5 線程B >>> 5
 6 線程B >>> 6
 7 線程B >>> 7
 8 線程B >>> 8
 9 線程B >>> 9
10 線程B >>> 10

結果說明:spa

由於兩個線程的調度徹底受CPU時間片的影響,只有當一個線程運行時間結束後,另外一個線程才能運行,並不能實如今線程運行的過程當中進行切換。線程

若是咱們想讓兩個線程交替打印數字,那麼很顯然同步機制是作不到的,這時候就須要兩個線程的協做,讓兩個線程之間進行通訊。

線程的等待喚醒機制

要達到上面所說的兩個線程交替打印,須要兩個線程進行通訊,當第一個線程打印了以後,把本身鎖起來,喚醒第二個線程打印,當第二個線程打印以後,也把本身鎖起來,喚醒第一個線程,這樣就能夠實現兩個線程的交替打印了。

線程的協做就是線程的通訊,好比有A和B兩個線程,A和B均可以獨立運行,A和B有時也會作信息的交換,這就是線程的協做了。在Java裏線程的協做是經過Object類裏的wait()和notify()/和notifyAll()來實現的。

涉及的方法

◆ wait()

該方法會致使當前線程等待,直到其餘線程調用了此線程的notify()或者notifyAll()方法。注意到wait()方法會拋出異常,因此在面咱們的代碼中要對異常進行捕獲處理。

◆ wait(long timeout)

該方法與wait()方法相似,惟一的區別就是在指定時間內,若是沒有notify或notifAll方法的喚醒,也會自動喚醒。wait(0)等效於wait()。

◆ nofity()

喚醒線程池中任意一個線程。

◆ notifyAll()

喚醒線程池中的全部線程。

方法的使用說明

這些方法都必須定義在同步中。由於這些方法是用於操做線程狀態的方法,因此必需要明確到底操做的是哪一個鎖上的線程。

注意到上述操做線程的方法都是放在Object類中,這是由於方法都是同步鎖的方法。而鎖能夠是任意對象,任意的對象均可以調用的方法必定定義在Object類中。

這些方法屬於Object類的一部分而不是Thread類的一部分,這個咋看一下真的很奇怪,不過細細思考下,這種作法是有道理的,咱們把鎖機制授予對象會幫咱們擴展線程應用的思路,至少把這幾個方法用在任何的具備同步控制功能的方法中,而不用去考慮方法所在的類是繼承了Thread仍是實現了Runnable接口。
可是事實上使用這些方法仍是要注意只能在同步控制方法同步塊裏調用,由於這些操做都會使用到鎖。

若是是在非同步的方法裏調用這些方法,程序會編譯經過,可是在運行時候程序會報出IllegalMonitorStateException異常,這個異常的含義是調用方法的線程在調用這些方法前必須擁有這個對象的鎖,或者當前調用方法的對象鎖不是以前同步時的那個鎖

使用喚醒等待實現兩個線程交替執行

代碼以下:

 1 public class Demo {
 2     public static void main(String[] args) {
 3         DemoThread demoThread = new DemoThread();
 4         Thread a = new Thread(demoThread, "線程A");
 5         Thread b = new Thread(demoThread, "線程B");
 6         a.start();
 7         b.start();
 8     }
 9 }
10 
11 class DemoThread implements Runnable {
12     private Integer num = 1;
13 
14     @Override
15     public void run() {
16         while (true) {
17             synchronized (DemoThread.class) {
18                 DemoThread.class.notify();
19                 if (num <= 10) {
20                     System.out.println(Thread.currentThread().getName() + " >>> " + num++);
21                     try {
22                         DemoThread.class.wait();
23                     } catch (InterruptedException e) {
24                         e.printStackTrace();
25                     }
26                 }
27             }
28         }
29     }
30 }

運行結果以下:

 1 線程A >>> 1
 2 線程B >>> 2
 3 線程A >>> 3
 4 線程B >>> 4
 5 線程A >>> 5
 6 線程B >>> 6
 7 線程A >>> 7
 8 線程B >>> 8
 9 線程A >>> 9
10 線程B >>> 10

線程的虛假喚醒

在使用wait()時,當被喚醒時有可能會被虛假喚醒,建議使用while而不是if來進行判斷,即在循環中使用wait()方法。

在下面的代碼中,沒有在循環中使用wait()方法:

 1 public class Demo {
 2     public static void main(String[] args) {
 3         DemoThread demoThread = new DemoThread();
 4         
 5         Thread a = new Thread(new Runnable() {
 6             @Override
 7             public void run() {
 8                 for (int i = 0; i < 4; i++) {
 9                     demoThread.increase();
10                 }
11             }
12         }, "線程A");
13         Thread b = new Thread(new Runnable() {
14             @Override
15             public void run() {
16                 for (int i = 0; i < 4; i++) {
17                     demoThread.decrease();
18                 }
19             }
20         }, "線程B");
21         Thread c = new Thread(new Runnable() {
22             @Override
23             public void run() {
24                 for (int i = 0; i < 4; i++) {
25                     demoThread.increase();
26                 }
27             }
28         }, "線程C");
29         Thread d = new Thread(new Runnable() {
30             @Override
31             public void run() {
32                 for (int i = 0; i < 4; i++) {
33                     demoThread.decrease();
34                 }
35             }
36         }, "線程D");
37         a.start();
38         b.start();
39         c.start();
40         d.start();
41     }
42 }
43 
44 class DemoThread {
45     private static Integer num = 1;
46     
47     public synchronized void increase() {
48         if (num > 0) {
49             try {
50                 this.wait();
51             } catch (InterruptedException e) {
52                 e.printStackTrace();
53             }
54         }
55         num++;
56         System.out.print(num + " ");
57         this.notifyAll();
58     }
59     
60     public synchronized void decrease() {
61         if (num == 0) {
62             try {
63                 this.wait();
64             } catch (InterruptedException e) {
65                 e.printStackTrace();
66             }
67         }
68         num--;
69         System.out.print(num + " ");
70         this.notifyAll();
71     }
72 }

運行結果以下:

1 0 1 0 1 2 1 0 1 2 1 0 1 2 1 2 1

能夠看到即使使用了synchronized關鍵字,仍然出現了線程安全問題,緣由以下:

在某一刻,一個負責增長的線程得到了資源,此時num爲1,因此執行 this.wait(); 並等待。

下一刻,另外一個負責增長的線程得到了資源,此時num仍然爲1,因此再次執行 this.wait(); 並等待。

此後負責減小的線程將num減小到0並喚醒全部等待進程,兩個負責增長的線程被喚醒,執行兩次增長運算,致使num爲2的狀況產生。

解決辦法就是將 if (num > 0) { 和 if (num == 0) { 中的if換成while。

wait()和sleep()方法的區別

兩個方法聲明的位置不一樣:Thread類中聲明sleep() ,Object類中聲明wait()。

使用方法不一樣:wait()能夠指定時間,也能夠不指定時間,sleep()必須指定時間。

調用的要求不一樣:sleep()能夠在任何須要的場景下調用, wait()必須使用在同步代碼塊或同步方法中。

關因而否釋放同步監視器:若是兩個方法都使用在同步代碼塊或同步方法中,sleep()不會釋放鎖,wait()會釋放鎖。

生產者消費者問題

 1 public class Demo {
 2     public static void main(String[] args) {
 3         Clerk clerk = new Clerk();
 4         Productor productor = new Productor(clerk);
 5         Consumer consumer = new Consumer(clerk);
 6         Thread productor1 = new Thread(productor, "生產者1");
 7         Thread productor2 = new Thread(productor, "生產者2");
 8         Thread consumer1 = new Thread(consumer, "消費者1");
 9         Thread consumer2 = new Thread(consumer, "消費者2");
10         productor1.start();
11         productor2.start();
12         consumer1.start();
13         consumer2.start();
14     }
15 }
16 
17 class Clerk {// 店員
18     private int num = 0;// 產品數量
19 
20     public synchronized void product() {// 生產產品
21         if (num < 2000) {
22             System.out.println(Thread.currentThread().getName() + ":生產了第" + ++num + "個產品");
23             notifyAll();
24         } else {
25             try {
26                 wait();
27             } catch (InterruptedException e) {
28                 e.printStackTrace();
29             }
30         }
31     }
32 
33     public synchronized void consume() {// 消費產品
34         if (num > 0) {
35             System.out.println(Thread.currentThread().getName() + ":消費了第" + num-- + "個產品");
36             notifyAll();
37         } else {
38             try {
39                 wait();
40             } catch (InterruptedException e) {
41                 e.printStackTrace();
42             }
43         }
44     }
45 }
46 
47 class Productor implements Runnable {// 生產者線程
48     Clerk clerk;
49 
50     public Productor(Clerk clerk) {
51         this.clerk = clerk;
52     }
53 
54     @Override
55     public void run() {
56         System.out.println(Thread.currentThread().getName() + "開始生產產品");
57         while (true) {
58             clerk.product();
59         }
60     }
61 }
62 
63 class Consumer implements Runnable {// 消費者線程
64     Clerk clerk;
65 
66     public Consumer(Clerk clerk) {
67         this.clerk = clerk;
68     }
69 
70     @Override
71     public void run() {
72         System.out.println(Thread.currentThread().getName() + "開始消費產品");
73         while (true) {
74             clerk.consume();
75         }
76     }
77 }
相關文章
相關標籤/搜索