34-多執行緒--死鎖+執行緒間通訊+等待喚醒機制+多生產者多消費者問題
一、死鎖
1、死鎖的常見情形之一:同步的巢狀
說明:同步的巢狀,至少得有兩個鎖,且第一個鎖中有第二個鎖,第二個鎖中有第一個鎖。eg:同步程式碼塊中有同步函式,同步函式中有同步程式碼塊。下面的例子,同步程式碼塊的鎖是obj,同步函式的鎖是this。t1執行緒先執行同步程式碼塊,獲取鎖obj,需要鎖this才能執行同步函式;而t2執行緒先執行同步函式,獲取鎖this,需要鎖obj才能執行同步程式碼塊。兩個執行緒相互競爭鎖資源,可能和諧的交替執行到最後,也可能會發生死鎖
死鎖示例:t1執行緒執行 if 中的內容,先獲取obj鎖,再獲取this鎖,接著執行show()方法中的內容。之後出show(),釋放this鎖,但還沒有出同步程式碼塊,沒有釋放obj鎖。此時,t2執行緒開始執行,走 else 中的內容,拿到this鎖,想要獲取obj鎖。但obj鎖被t1執行緒持有,造成死鎖:t1執行緒持有obj鎖,想要this鎖;而t2執行緒持有this鎖,想要obj鎖。程式掛在這裡執行不了
class Ticket implements Runnable { //票 private int num = 400; //同步鎖 private Object obj = new Object(); //標誌位 boolean flag = true; @Override public void run() { if (flag) { while (true) { //同步程式碼塊中有同步函式 //同步程式碼塊,鎖是obj synchronized (obj) { //同步函式,鎖是this show(); } } } else { while (true) { this.show(); } } } /** * 同步函式,鎖是this * 同步函式中有同步程式碼塊 */ public synchronized void show() { //同步程式碼塊,鎖是obj synchronized (obj) { if (num > 0) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "......" + num--); } } } } public class Test { public static void main(String[] args) { Ticket t = new Ticket(); Thread t1 = new Thread(t); Thread t2 = new Thread(t); t1.start(); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } t.flag = false; t2.start(); } }
2、要避免死鎖的發生。不容易找到問題在哪兒,發現問題也不好解決
3、寫死鎖遵循的原則只有一個:巢狀
class Lock { /** * static:靜態的,類名直接呼叫即可 * final:鎖固定不變 * 巢狀至少得有兩個鎖 */ public static final Object loakA = new Object(); public static final Object loakB = new Object(); } class DeadLock implements Runnable { //標誌位 private boolean flag; //通過建構函式給標誌位賦值 //否則得在主函式中做切換,還得sleep(time),麻煩(見上例) DeadLock(boolean flag) { this.flag = flag; } @Override public void run() { if (flag) { //加 while迴圈 是為了讓程式碼多執行幾次,以便出現想要的結果 while (true) { synchronized (Lock.loakA) { System.out.println(Thread.currentThread().getName() + "...if...lockA..."); synchronized (Lock.loakB) { System.out.println(Thread.currentThread().getName() + "...if...lockB..."); } } } } else { while (true) { synchronized (Lock.loakB) { System.out.println(Thread.currentThread().getName() + "...else...lockB..."); synchronized (Lock.loakA) { System.out.println(Thread.currentThread().getName() + "...else...lockA..."); } } } } } } public class Test { public static void main(String[] args) { /** * 1、用建構函式給標誌位賦值,做標誌位的切換 * 主函式中就不用 切換+sleep(time) 了 * * 2、執行緒任務是一個物件,多個執行緒操作同一個執行緒任務 * 此處new兩個執行緒任務物件a、b,兩個執行緒t1、t2執行兩個執行緒任務a、b,沒問題 * 因為兩個執行緒任務a、b執行的程式碼都是run()方法,雖然兩個物件都有自己的flag,但它們的flag值是固定的,一個是true,一個是false * 只有這種情況,可以封裝多個任務 * * 3、執行緒任務封裝的資源是布林型的變數,這個變數雖然在兩個執行緒任務物件中都有獨立的一份,但取的值就只能是true或false * 如果變數不是布林型,是int,就導致有兩個執行緒任務 * (此處可以封裝多個執行緒任務,是因為DeadLock中除了flag沒有其他變數,且flag的值也是固定的(建構函式傳參) * 只有這種情況才可以封裝多個執行緒任務) */ DeadLock dl1 = new DeadLock(true); DeadLock dl2 = new DeadLock(false); Thread t1 = new Thread(dl1); Thread t2 = new Thread(dl2); t1.start(); t2.start(); } }
二、執行緒間通訊
1、執行緒間通訊:多個執行緒在處理同一資源,但執行緒任務不同(之前賣票和存錢的示例,都是多個執行緒在執行同一個執行緒任務,即 只有一個run()方法)
2、需求:有一個資源Rsoource,裡面有兩個屬性name和sex。希望輸入和輸出輪流且不重複進行,即輸入一次,輸出一次;再輸入一次,再輸出一次......
(1)程式碼示例
/**
* 建立一個類來描述資源
* 資源是共享的,操作資源中屬性(共享資料)的語句有多條,就存線上程安全問題
*/
class Resource {
String name;
String sex;
//標誌位,標記資源中有無資料。預設資源中沒有資料,flag=false
//規則:沒有資料,就輸入;有資料,就輸出
//所以,輸入和輸出前,都要先判斷資源中有無資料
boolean flag = false;
}
/**
* 兩個任務物件run()要分別封裝在兩個類中
* 要封裝執行緒任務,需要實現Runnable介面
*/
class Input implements Runnable {
//此處不能new Resource(),否則輸入和輸出兩個執行緒用的不是同一個資源
//只能將資源Resource r作為引數傳遞進來。能接收引數傳遞的兩種形式:一般方法和建構函式
//因為執行緒任務物件一初始化就有資源,所以,使用建構函式傳遞引數Resource r
private Resource r;
Input(Resource r) {
this.r = r;
}
@Override
public void run() {
//此處使用 int x 變數模擬多使用者的切換:%2
//還可以使用 boolean flag 變數做切換,flag = !flag
int x = 0;
while (true) {
//為了保證輸入和輸出執行緒使用同一個鎖,可以用Resource r作為鎖,也可以用靜態鎖Resource.class等
//鎖不能用this,因為this代表本類物件,而輸入和輸出兩個執行緒在兩個類中
synchronized (r) {
//輸入前,要先判斷資源中有無資料
//如果有資料,不用再輸入。此時,Input要停一下,等待Output先輸出
//使用r.flag只代表flag是Resource r中的一個屬性,可以省略r.
if (r.flag) {
try {
//Input被wait()後,處於凍結狀態,釋放執行權的同時釋放執行資格,只能等待被notify()喚醒
//使用wait()、notify()、notifyAll()方法時,必須要明確自己所屬的鎖,否則會報錯
r.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果沒有資料,輸入
if (x == 0) {
r.name = "mike";
r.sex = "nan";
} else {
r.name = "麗麗";
r.sex = "女";
}
//修改標誌位。此時,資源中有資料了
//使用r.flag只代表flag是Resource r中的一個屬性,可以省略r.
r.flag = true;
//喚醒輸出Output執行緒(因為Input即將進入等待)
//如果Output沒有進入等待狀態,可以是空喚醒一次
//使用wait()、notify()、notifyAll()方法時,必須要明確自己所屬的鎖,否則會報錯
r.notify();
//模擬多使用者的切換
x = (x + 1) % 2; //等價於:x = x++ % 2;
}
}
}
}
/**
* 兩個任務物件run()要分別封裝在兩個類中
* 要封裝執行緒任務,需要實現Runnable介面
*/
class Output implements Runnable {
//此處不能new Resource(),否則輸入和輸出兩個執行緒用的不是同一個資源
//只能將資源Resource r作為引數傳遞進來。能接收引數傳遞的兩種形式:一般方法和建構函式
//因為執行緒任務物件一初始化就有資源,所以,使用建構函式傳遞引數Resource r
private Resource r;
Output(Resource r) {
this.r = r;
}
@Override
public void run() {
while (true) {
//為了保證輸入和輸出執行緒使用同一個鎖,可以用Resource r作為鎖。也可以用靜態鎖Resource.class等
//鎖不能用this,因為this代表本類物件,而輸入和輸出兩個執行緒在兩個類中
synchronized (r) {
//輸出前,要先判斷資源中有無資料
//如果沒有資料,不用再輸出。此時,Output要停一下,等待Input先輸入
//使用r.flag只代表flag是Resource r中的一個屬性,可以省略r.
if (!r.flag) {
try {
//Output被wait()後,處於凍結狀態,釋放執行權的同時釋放執行資格,只能等待被notify()喚醒
//使用wait()、notify()、notifyAll()方法時,必須要明確自己所屬的鎖,否則會報錯
r.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果有資料,輸出
System.out.println(r.name + "......" + r.sex);
//修改標誌位。此時,資源中沒有資料了
//使用r.flag只代表flag是Resource r中的一個屬性,可以省略r.
r.flag = false;
//喚醒輸入Input執行緒(因為Output即將進入等待)
//使用wait()、notify()、notifyAll()方法時,必須要明確自己所屬的鎖,否則會報錯
r.notify();
}
}
}
}
public class Test {
public static void main(String[] args) {
//建立資源
Resource r = new Resource();
//建立執行緒任務。建立任務要明確資源
Input input = new Input(r);
Output output = new Output(r);
//建立執行緒。建立執行緒要明確任務
Thread t1 = new Thread(input);
Thread t2 = new Thread(output);
//開啟執行緒
t1.start();
t2.start();
}
}
分析:Input和Output共用資源Resource,所以,操作Resource中屬性的多條語句需要加同步。主執行緒首先開啟Input,執行其run()方法。判斷資源中沒有資料flag=false,向其中輸入。之後將flag置為true,表示資源中已經有資料了。同時,喚醒Output(Input馬上要進入凍結狀態了)。此次迴圈結束,進入下次迴圈(while(true){...})。判斷flag=true,執行wait()方法,Input被凍結。等到Output獲取到CPU的執行權,Output執行。先判斷flag=true,表示資源中有資料,輸出。然後,將flag置為false,表示資源中沒有資料。同時,喚醒Input(Output馬上要進入凍結狀態了)。此次迴圈結束,進入下次迴圈(while(true){...})。判斷flag=false,執行wait()方法,Output被凍結。等到Input獲取到CPU的執行權,再次重複上述操作......
注:建立執行緒,要明確任務;建立任務,要明確資源
(2)優化後的程式碼
class Resource {
//資源的屬性通常都是私有的,並對外提供訪問方法
private String name;
private String sex;
private boolean flag = false;
//不單獨寫setName()、getName()方法,因為準備同時賦值
//使用同步函式,用this鎖更簡單
public synchronized void set(String name, String sex) {
if (flag) {
try {
//此時使用的鎖是this,代表資源Resource物件
//wait()和notify()方法一定要所屬於同步。因為它本身是鎖上的方法,用來操作指定鎖上的執行緒的方法
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name;
this.sex = sex;
flag = true;
//wait()和notify()方法一定要所屬於同步。因為它本身是鎖上的方法,用來操作指定鎖上的執行緒的方法
//this可省略
this.notify();
}
//使用同步函式,用this鎖更簡單
public synchronized void out() {
if (!flag) {
try {
//wait()和notify()方法一定要所屬於同步。因為它本身是鎖上的方法,用來操作指定鎖上的執行緒的方法
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(name + "......" + sex);
flag = false;
//wait()和notify()方法一定要所屬於同步。因為它本身是鎖上的方法,用來操作指定鎖上的執行緒的方法
this.notify();
}
}
class Input implements Runnable {
private Resource r;
Input(Resource r) {
this.r = r;
}
@Override
public void run() {
int x = 0;
while (true) {
if (x == 0) {
r.set("mike", "nan");
} else {
r.set("麗麗", "女");
}
x = (x + 1) % 2;
}
}
}
class Output implements Runnable {
private Resource r;
Output(Resource r) {
this.r = r;
}
@Override
public void run() {
while (true) {
r.out();
}
}
}
public class Test {
public static void main(String[] args) {
Resource r = new Resource();
Input input = new Input(r);
Output output = new Output(r);
Thread t1 = new Thread(input);
Thread t2 = new Thread(output);
t1.start();
t2.start();
}
}
說明:資源裡面封裝了屬性(私有的),同時對外提供了訪問資源的方法。如果需要同步,在資源所提供的方法中加上同步即可
注:wait()和notify()方法一定要所屬於同步。因為它本身是鎖上的方法,用來操作指定鎖上的執行緒的方法
三、等待喚醒機制
1、等待/喚醒機制涉及的方法
(1)wait():讓執行緒處於凍結狀態,被wait()的執行緒會被儲存到執行緒池中(被wait()的執行緒沒有消亡,但失去了CPU的執行資格,儲存線上程池中)
注:執行緒池按照鎖來區分
(2)notify():喚醒執行緒池中的任意一個執行緒
(3)notifyAll():喚醒執行緒池中的所有執行緒(讓執行緒池中的執行緒都處於執行狀態或者臨時阻塞狀態,即 讓執行緒池中的執行緒具備CPU的執行資格)
2、使用wait()、notify()、notifyAll()方法的注意事項
(1)wait()、notify()、notifyAll()這些方法都必須定義在同步synchronized(物件){...}中。因為這些方法是用於操作執行緒狀態的方法(監視執行緒的狀態),一旦執行緒狀態發生改變,必須要明確改變的是哪個鎖上的執行緒。所以,在呼叫這些方法時,要標識出該方法所屬的鎖
(2)使用wait()、notify()、notifyAll()方法時,必須要明確自己所屬的鎖,否則會報錯
(3)wait()、notify()、notifyAll()不是執行緒類Thread中的方法,而是Object中的方法。因為當前執行緒必須擁有此物件監視器,就是鎖。而鎖可以是任意的物件,任意的物件呼叫的方法一定定義在Object類中
四、多生產者多消費者問題
生產者生產烤鴨,消費者消費烤鴨,且烤鴨帶編號
1、需求:一個生產者和一個消費者,每次 生產/消費 一隻烤鴨。只有沒有烤鴨時才生產,只有有烤鴨時才消費,其餘時間均等待
class Resource {
private String name;
private int count = 1;
//標記
private boolean flag = false;
//傳入:名稱,得到:名稱+編號
//可以使用this鎖,所以,使用同步程式碼塊較簡單
public synchronized void set(String name) {
if (flag) {
try {
//使用wait()、notify()、notifyAll()要明確所屬的鎖
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name + count;
count++;
System.out.println(Thread.currentThread().getName() + "...生產者..." + this.name);
this.flag = true;
//使用wait()、notify()、notifyAll()要明確所屬的鎖
this.notify();
}
public synchronized void out() {
if (!flag) {
try {
//使用wait()、notify()、notifyAll()要明確所屬的鎖
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "......消費者......" + this.name);
this.flag = false;
//使用wait()、notify()、notifyAll()要明確所屬的鎖
this.notify();
}
}
class Producer implements Runnable {
private Resource r;
Producer(Resource r) {
this.r = r;
}
@Override
public void run() {
while (true){
r.set("烤鴨");
}
}
}
class Consumer implements Runnable {
private Resource r;
Consumer(Resource r) {
this.r = r;
}
@Override
public void run() {
while (true){
r.out();
}
}
}
public class Test {
public static void main(String[] args) {
Resource r = new Resource();
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(con);
t1.start();
t2.start();
}
}
2、需求:兩個生產者和兩個消費者,每次 生產/消費 一隻烤鴨。只有沒有烤鴨時才生產,只有有烤鴨時才消費,其餘時間均等待
public class Test {
public static void main(String[] args) {
Resource r = new Resource();
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
//執行緒封裝任務,任務封裝資源
//兩個生產者
Thread t0 = new Thread(pro);
Thread t1 = new Thread(pro);
//兩個消費者
Thread t2 = new Thread(con);
Thread t3 = new Thread(con);
t0.start();
t1.start();
t2.start();
t3.start();
問題一:當多個執行緒生產/消費時,出現了問題:生產一堆烤鴨未被消費,或多次消費同一編號的烤鴨
分析:
(1)生產者t0得到CPU的執行權,進入set()方法,判斷flag=false,生產一隻烤鴨(生產者...烤鴨1)。之後將flag置為true,執行notify()空喚醒,此次迴圈結束
(2)因為run()方法中的while(true),進入下次迴圈,t0再次執行set()方法。此時,flag=true,執行try程式碼塊。t0被wait(),失去CPU的執行資格,進入執行緒池等待
(3)生產者t1得到CPU的執行權,進入set()方法,判斷flag=true,執行try程式碼塊。t1被wait(),失去CPU的執行資格,進入執行緒池等待
(4)消費者t2得到CPU的執行權,進入out()方法,判斷flag=true,消費一隻烤鴨(消費者...烤鴨1)。之後將flag置為false,執行notify(),喚醒生產者執行緒t0,此次迴圈結束
(5)因為run()方法中的while(true),進入下次迴圈,t2再次執行out()方法。此時,flag=false,執行try程式碼塊。t2被wait(),失去CPU的執行資格,進入執行緒池等待
(6)消費者t3得到CPU的執行權,進入out()方法,判斷flag=false,執行try程式碼塊。t3被wait(),失去CPU的執行資格,進入執行緒池等待
(7)此時只有被喚醒的生產者t0還活著
(8)生產者t0得到CPU的執行權,從wait()處向下走(不再判斷if(flag)),生產一隻烤鴨(生產者...烤鴨2)。之後將flag置為true,執行notify(),喚醒生產者執行緒t1,此次迴圈結束
(9)因為run()方法中的while(true),進入下次迴圈,t0再次執行set()方法。此時,flag=true,執行try程式碼塊。t0被wait(),失去CPU的執行資格,進入執行緒池等待
(10)此時只有被喚醒的生產者t1還活著
(11)生產者t1得到CPU的執行權,從wait()處向下走(不再判斷if(flag)),生產一隻烤鴨(生產者...烤鴨3)...... 出現問題:生產的烤鴨2未被消費就生產了烤鴨3
原因:被喚醒的執行緒沒有再次判斷 if(flag) 標記,就直接執行下面的程式碼,繼續進行生產/消費
做法:將 if(flag) 改為 while(flag),讓醒來的執行緒重新判斷標誌位flag
問題二:程式死鎖
分析(從上面第(8)步開始):
(8)生產者t0得到CPU的執行權,從wait()處醒來。因為while(flag),會再次判斷flag=false,生產一隻烤鴨(生產者...烤鴨2)。之後將flag置為true,執行notify(),喚醒生產者執行緒t1,此次迴圈結束
(9)因為run()方法中的while(true),進入下次迴圈,t0再次執行set()方法。此時,flag=true,執行try程式碼塊。t0被wait(),失去CPU的執行資格,進入執行緒池等待
(10)此時只有被喚醒的生產者t1還活著
(11)生產者t1得到CPU的執行權,從wait()處醒來。因為while(flag),會再次判斷flag=true,執行try程式碼塊。t1被wait(),失去CPU的執行資格,進入執行緒池等待
(12)此時,已經沒有活著的執行緒了,所有執行緒都被凍結,線上程池等待
原因:notify()喚醒的是任意一方,沒有對方導致程式死鎖
做法:用notifyAll(),全喚醒(必須要喚醒對方)
正確的程式碼:
class Resource {
private String name;
private int count = 1;
//標誌位
private boolean flag = false;
//傳入:名稱,得到:名稱+編號
//可以使用this鎖,所以,使用同步程式碼塊較簡單
public synchronized void set(String name) {
//用 while 而不是 if ,是為了讓醒來的執行緒重新判斷標誌位
while (flag) {
try {
//使用wait()、notify()、notifyAll()要明確所屬的鎖
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name + count;
count++;
System.out.println(Thread.currentThread().getName() + "...生產者..." + this.name);
this.flag = true;
//使用wait()、notify()、notifyAll()要明確所屬的鎖
//全喚醒,避免死鎖
this.notifyAll();
}
public synchronized void out() {
//用 while 而不是 if ,是為了讓醒來的執行緒重新判斷標誌位
while (!flag) {
try {
//使用wait()、notify()、notifyAll()要明確所屬的鎖
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "......消費者......" + this.name);
this.flag = false;
//使用wait()、notify()、notifyAll()要明確所屬的鎖
//全喚醒,避免死鎖
this.notify();
}
}
class Producer implements Runnable {
private Resource r;
Producer(Resource r) {
this.r = r;
}
@Override
public void run() {
while (true) {
r.set("烤鴨");
}
}
}
class Consumer implements Runnable {
private Resource r;
Consumer(Resource r) {
this.r = r;
}
@Override
public void run() {
while (true) {
r.out();
}
}
}
public class Test {
public static void main(String[] args) {
//執行緒封裝任務,任務封裝資源
//資源
Resource r = new Resource();
//執行緒任務
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
//執行緒
//兩個生產者
Thread t0 = new Thread(pro);
Thread t1 = new Thread(pro);
//兩個消費者
Thread t2 = new Thread(con);
Thread t3 = new Thread(con);
t0.start();
t1.start();
t2.start();
t3.start();
}
}
總結:
(1)while:判斷標記,多次判斷。解決了執行緒獲取執行權後,是否要執行的問題
(2)notifyAll():喚醒全部。保證本方執行緒一定會喚醒對方執行緒,避免死鎖
(3)if:判斷標記,只判斷一次。導致不該執行的執行緒執行,出現了資料錯誤的情況
(4)notify():只能喚醒一個執行緒。如果本方喚醒了本方,沒有意義。而且 while判斷標記+notify() 會導致死鎖
依舊存在的問題:
用 while判斷標記+notifyAll() ,程式效率有點低。全喚醒時,本方也醒了,而本方判斷標記沒有意義。只喚醒對方即可