通過兩個小栗子來說說Java的sleep、wait、notify、notifyAll的用法


線程是計算程序運行的最小載體,由於單個單核CPU的硬件水平發展到了一定的瓶頸期,因此就出現了多核多CPU的情況,直接就導致程序員多線程編程的復雜。由此可見線程對於高性能開發的重要性。

那么線程在計算機中有好幾種狀態,他們之間是怎么切換的?sleep和wait又有什么區別?notify和notifyAll怎么用?帶着這些問題,我們來看看Java的線程吧!

Thread的狀態

先來看看Thread類里面都有哪幾種狀態,在Thread.class中可以找到這個枚舉,它定義了線程的相關狀態:

public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

如下圖所示:

  1. NEW 新建狀態,線程創建且沒有執行start方法時的狀態
  2. RUNNABLE 可運行狀態,線程已經啟動,但是等待相應的資源(比如IO或者時間片切換)才能開始執行
  3. BLOCKED 阻塞狀態,當遇到synchronized或者lock且沒有取得相應的鎖,就會進入這個狀態
  4. WAITING 等待狀態,當調用Object.wait或者Thread.join()且沒有設置時間,在或者LockSupport.park時,都會進入等待狀態。
  5. TIMED_WAITING 計時等待,當調用Thread.sleep()或者Object.wait(xx)或者Thread.join(xx)或者LockSupport.parkNanos或者LockSupport.partUntil時,進入該狀態
  6. TERMINATED 終止狀態,線程中斷或者運行結束的狀態

先來sleep和wait的區別

由於wait方法是在Object上的,而sleep方法是在Thread上,當調用Thread.sleep時,並不能改變對象的狀態,因此也不會釋放鎖。

這讓我想起來我家的兩個主子,一只泰迪一只美短,雖然他們兩個是不同的物種,但是卻有着相同的愛好,就是愛吃牛肉。偶爾給它們兩個開葷,奈何只有一個食盆,每次只能一個主子吃肉。這就好比是兩個線程,在爭用同一個變量。如果使用thread.sleep,那么其中一個吃完一塊肉后,會霸占食盆,不給另一只吃(不會釋放鎖等資源);如果使用wait,那么吃肉時,會離開食盆,這樣就有機會讓另一只去吃了,即占用的資源會釋放。

詳細的看一下下面的代碼:

package cn.xingoo.test.basic.thread;

public class AnimalEat {

    public static void main(String[] args) {
        System.out.println("盆里有20塊肉");
        Animal animal = new Animal();
        try{
            Thread tidy = new Thread(animal,"泰迪");
            Thread cat  = new Thread(animal,"美短");
            tidy.start();
            cat.start();
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println("盆里的肉吃完了!");
    }


}
class Animal implements Runnable {
    int count = 0;

    @Override
    public void run() {
        while(count < 20){
            synchronized (this){
                try {
                    System.out.println(Thread.currentThread().getName()+"吃力第"+count+"塊肉");
                    count++;
                    //Thread.sleep(100);
                    this.wait(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

當使用this.wait(100)的時候,會輸出下面的信息:

盆里有20塊肉
泰迪吃力第0塊肉
美短吃力第1塊肉
盆里的肉吃完了!
泰迪吃力第2塊肉
美短吃力第3塊肉
泰迪吃力第4塊肉
美短吃力第5塊肉
泰迪吃力第6塊肉
美短吃力第7塊肉
泰迪吃力第8塊肉
美短吃力第9塊肉
泰迪吃力第10塊肉
美短吃力第11塊肉
美短吃力第12塊肉
泰迪吃力第13塊肉
美短吃力第14塊肉
泰迪吃力第15塊肉
美短吃力第16塊肉
泰迪吃力第17塊肉
美短吃力第18塊肉
泰迪吃力第19塊肉

可以發現,輸出的信息並不是完美的交替,這是因為調用wait之后,並不一定馬上時另一個線程執行,而是要根據CPU的時間分片輪轉等其他的條件來定,輪到誰就看運氣了。

當使用Thread.sleep(100)的時候,可以得到下面的信息:

盆里有20塊肉
泰迪吃力第0塊肉
盆里的肉吃完了!
泰迪吃力第1塊肉
泰迪吃力第2塊肉
泰迪吃力第3塊肉
泰迪吃力第4塊肉
泰迪吃力第5塊肉
泰迪吃力第6塊肉
泰迪吃力第7塊肉
泰迪吃力第8塊肉
泰迪吃力第9塊肉
泰迪吃力第10塊肉
泰迪吃力第11塊肉
泰迪吃力第12塊肉
泰迪吃力第13塊肉
泰迪吃力第14塊肉
泰迪吃力第15塊肉
泰迪吃力第16塊肉
泰迪吃力第17塊肉
泰迪吃力第18塊肉
美短吃力第19塊肉
泰迪吃力第20塊肉

注意看最后面有一只美短。這是因為synchronized的代碼同步時在while循環里面,因此最后一次兩個主子都進入到了while里面,然后才開始等待相應的鎖。這就導致第19次輪到了另一個主子。

總結來說,sleep不會釋放線程的鎖,wait會釋放線程的資源。

再談談wait與notify和notifyall

wait、notify、notifyall這幾個一般都一起使用。不過需要注意下面幾個重要的點:

  1. 調用wait\notify\notifyall方法時,需要與鎖或者synchronized搭配使用,不然會報錯java.lang.IllegalMonitorStateException,因為任何時刻,對象的控制權只能一個線程持有,因此調用wait等方法的時候,必須確保對其的控制權。
  2. 如果對簡單的對象調用wait等方法,如果對他們進行賦值也會報錯,因為賦值相當於修改的原有的對象,因此如果有修改需求可以外面包裝一層。
  3. notify可以喚醒一個在該對象上等待的線程,notifyAll可以喚醒所有等待的線程。
  4. wait(xxx) 可以掛起線程,並釋放對象的資源,等計時結束后自動恢復;wait()則必須要其他線程調用notify或者notifyAll才能喚醒。

舉個通俗點的例子,我記得在高中的時候,每天上午快放學的時候大家都很緊張——因為那個時候小飯館正好播放一些港台劇,大家就總願意搶電視機旁邊的位置,所以每次快要中午放學的時候,大家都做好沖刺跑步的准備。

但是有的老師總願意壓堂,搞的大家怨聲載道。

比如,下面這位老師有的時候會用notifyall通知大家集體放學;有的時候會檢查背書,背好了,才能走。

package cn.xingoo.test.basic.thread;

public class School {
    private DingLing dingLing = new DingLing(false);

    class Teacher extends Thread{
        Teacher(String name){
            super(name);
        }
        @Override
        public void run() {
            synchronized (dingLing){
                try {
                    dingLing.wait(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                dingLing.flag = true;

                System.out.println("放學啦");
                dingLing.notifyAll();

                /*for (int i = 0; i < 3; i++) {
                    System.out.println("放一個走吧");
                    dingLing.notify();
                    try {
                        dingLing.wait(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }*/
             }
        }
    }
    class Student extends Thread{
        Student(String name){
            super(name);
        }
        @Override
        public void run(){
            synchronized (dingLing){
                while(!dingLing.flag){
                    System.out.println(Thread.currentThread().getName()+"開始等待");
                    try {
                        dingLing.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName()+"去吃飯啦");
            }
        }
    }

    public static void main(String[] args) {
        School school = new School();
        Teacher teacher  = school.new Teacher("老師");
        Student zhangsan = school.new Student("張三");
        Student lisi     = school.new Student("李四");
        Student wangwu   = school.new Student("王五");
        teacher.start();
        zhangsan.start();
        lisi.start();
        wangwu.start();
    }
}

class DingLing{
    Boolean flag = false;
    public DingLing(Boolean flag){
        this.flag = flag;
    }
}

當老師統一喊放學的時候,即調用dingLing.notifyAll();,會得到下面的輸出:

張三開始等待
李四開始等待
王五開始等待
放學啦
王五去吃飯啦
李四去吃飯啦
張三去吃飯啦

如果檢查背書,那么每次老師只會調用一次notify,讓一個同學(線程)走(工作),就會得到下面的輸出:

張三開始等待
李四開始等待
王五開始等待
放一個走吧
張三去吃飯啦
放一個走吧
李四去吃飯啦
放一個走吧
王五去吃飯啦

注意的是,調用wait可以釋放dingling的占用,這樣才能讓別的線程進行檢查,如果改成Thread.sleep,有興趣的童鞋就可以自己去看看效果啦!

參考

  1. 最簡單的實例說明wait、notify、notifyAll的使用方法:http://longdick.iteye.com/blog/453615
  2. Java sleep和wait的區別:http://www.jb51.net/article/113587.htm
  3. sleep和wait解惑:https://www.zhihu.com/question/23328075


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM