1. 程式人生 > >34-多執行緒--死鎖+執行緒間通訊+等待喚醒機制+多生產者多消費者問題

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() ,程式效率有點低。全喚醒時,本方也醒了,而本方判斷標記沒有意義。只喚醒對方即可