重走JAVA之路(六):你應該要知道的執行緒排程

散人丶發表於2019-04-02

作為Android開發者,老實說,平常關於一些執行緒排程的方法,用的確實不多,可能用的最多的也就是sleep作為一個休眠延時的操作,但是既然是Java之路,那就必須把那些東西拎出來說一說了,也是加強大家對執行緒的理解程度以及在處理執行緒中應該注意的問題。

1.join() 等待執行緒終止

這個方法大家可能用的不多,我們想象一個場景:主執行緒生成並起動了子執行緒,如果子執行緒裡要進行大量的耗時的運算,主執行緒往往將於子執行緒之前結束,但是如果主執行緒處理完其他的事務後,需要用到子執行緒的處理結果,也就是主執行緒需要等待子執行緒執行完成之後再結束,這個時候我們第一想法可能是要不然在子執行緒中處理完之後,用Handler把訊息傳到主執行緒再處理?這樣往往比較麻煩,這個時候就可以用join方法來實現

public class TestClass {
    public static void main(String []agrs){
       Thread thread = new Thread(new Runnable() {
           @Override
           public void run() {
               System.out.println("child thread start");
               try {
                   //模擬耗時操作
                   Thread.sleep(3000);
               } catch (InterruptedException mE) {
                   mE.printStackTrace();
               }
               System.out.println("child thread over");
           }
       });
       thread.start();
        try {
            thread.join();
        } catch (InterruptedException mE) {
            mE.printStackTrace();
        }
        System.out.println("main thread call");
    }
}
複製程式碼

執行,列印一波,可以看到呼叫join方法之後,主執行緒就可以在主執行緒執行完成之後,處理邏輯了

child thread start
child thread over
main thread call
Process finished with exit code 0
複製程式碼

2.wait()/notifyAll-----經典的生產者消費者問題

話不多說,直接上程式碼

public class Model {
    //為了觸發阻塞狀態,這裡把最大容量設定為1
    public static final int MAX_SIZE = 1;
    //儲存資料的集合
    public static LinkedList<Integer> list = new LinkedList<>();

    class Producer implements Runnable {
        @Override
        public void run() {
            synchronized (list) {
                //倉庫容量已經達到最大值
                while (list.size() == MAX_SIZE) {
                    System.out.println(Thread.currentThread().getName() + " no need to produce! repertory is full");
                    try {
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                list.add(1);
                System.out.println( Thread.currentThread().getName() + " produce,current repertory is " + list.size());
                list.notifyAll();
            }
        }
    }

    class Consumer implements Runnable {

        @Override
        public void run() {
            synchronized (list) {
                while (list.size() == 0) {
                    System.out.println(Thread.currentThread().getName() + " no product to consume! repertory is empty ");
                    try {
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                list.removeFirst();
                System.out.println(Thread.currentThread().getName() + " consume,current repertory is " + list.size());
                list.notifyAll();
            }
        }
    }

}
複製程式碼

主要程式碼就是呼叫wait/notifyAll方法,分別在極限情況時,對執行緒進行掛起以及喚醒,消費者和生產者開啟10個執行緒來測試一波

public class TestClass {
    public static void main(String []agrs){
       Model model = new Model();
       Model.Producer producer = model.new Producer();
       Model.Consumer consumer = model.new Consumer();
        for (int i = 0; i < 10; i++) {
            Thread proThread  = new Thread(producer);
            proThread.start();
            Thread conThread = new Thread(consumer);
            conThread.start();
        }
    }
}
複製程式碼
Thread-0 produce,current repertory is 1
Thread-2 no need to produce! repertory is full
Thread-1 consume,current repertory is 0
Thread-2 produce,current repertory is 1
Thread-3 consume,current repertory is 0
Thread-5 no product to consume! repertory is empty 
Thread-4 produce,current repertory is 1
Thread-5 consume,current repertory is 0
Thread-7 no product to consume! repertory is empty 
Thread-6 produce,current repertory is 1
Thread-7 consume,current repertory is 0
Thread-8 produce,current repertory is 1
Thread-9 consume,current repertory is 0
Thread-10 produce,current repertory is 1
Thread-11 consume,current repertory is 0
Thread-12 produce,current repertory is 1
Thread-13 consume,current repertory is 0
Thread-14 produce,current repertory is 1
Thread-15 consume,current repertory is 0
Thread-16 produce,current repertory is 1
Thread-17 consume,current repertory is 0
Thread-18 produce,current repertory is 1
Thread-19 consume,current repertory is 0

Process finished with exit code 0
複製程式碼

可以看到,當Thread-2要去生產時,發現此時倉庫以及滿了,此時呼叫wait方法,釋放鎖,同時執行緒阻塞,注意,這裡執行緒並不會結束掉,只是出於掛起狀態,當下次被喚醒時,會沿著wait方法後面繼續執行,在第四行也可以看到,當Thread-1消費了的時候,會呼叫notifyAll,此時喚醒所有在鎖池的物件,重新競爭獲取鎖,此時Thread-2又開始生產了

這種方法現象在Java中很常見,比如上篇執行緒池的文章也有提到過,裡面使用的阻塞佇列底層採用也是類似的機制,核心執行緒不會被回收被掛起,當有任務來時,喚醒執行緒去執行,有興趣的可以去看看重走JAVA之路(五):面試又被問執行緒池原理?教你如何反擊

再次總結一下:

  • 如果一個執行緒呼叫了wait方法,那麼該執行緒首先需要獲取到這個物件的鎖(換句話說,一個執行緒如果呼叫了某個方法的wait方法,那麼該wait方法必須是在synchronized方法中的)
  • 如果一個執行緒呼叫了wait方法,那麼當前執行緒就會釋放掉執行緒的鎖。(這個是wait和sleep方法不同的地方)
  • 在java中一個物件,會有兩個池(鎖池,等待池),如果一個執行緒呼叫了wait方法,那麼該執行緒進入該物件的等待池中(釋放鎖),如果未來的某一刻,另外一個執行緒呼叫了這個物件的notify方法,或者notifyAll,那麼在等待池中的執行緒就會起來進入該物件的鎖池中,參與到獲取鎖的競爭當中,如果獲取鎖成功,將沿著wait方法之後的程式碼執行(這就是程式碼中,使用while來判斷狀態,而不是if)。
  • notify和notifyAll方法並不會去釋放當前鎖物件,而是通過moniterexist來釋放,也就是說,當前所述的程式碼塊在執行結束之後,回去釋放掉鎖,只有在鎖被釋放掉之後,等待池中的執行緒進入到鎖池,去競爭鎖資源,所以一般notifyAll會防止同步程式碼的最後邊

我們知道android是基於訊息機制的,像之前的一個問題為什麼Looper.loop()死迴圈不會導致ANR一樣,主執行緒從佇列中讀取訊息,當沒有訊息時,主執行緒阻塞,讓出CPU,當訊息佇列中有訊息時,喚醒主執行緒,接著處理資料,所以 Looer.loop()方法可能會引起主執行緒的阻塞,但只要它的訊息迴圈沒有被阻塞,能一直處理事件就不會產生ANR異常。

3.interrupt() 停止執行緒

當需要終止一個執行緒時,Java給我們提供了2中方法,stop/interrupt,前者已經被廢棄了,也是不提倡呼叫的,一呼叫該方法,被stop的執行緒會馬上會釋放所有獲取的鎖並線上程的run()方法內,任何一點都有可能丟擲ThreadDeath Error,包括在catch或finally語句中,那麼很容易照成被同步的資料沒有被正確的處理完,那麼其它執行緒在讀取時就會得到髒資料

這裡主要講解interrupt方法, 首先我們要明白一點:

呼叫interrupt()方法,立刻改變的是中斷狀態,但如果不是在阻塞態,就不會丟擲異常;如果在進入阻塞態後,中斷狀態為已中斷,就會立刻丟擲異常,什麼叫阻塞態呢,大概就是呼叫了sleep,join,wait這幾個方法,其實在原始碼方法註釋上面也可以看到這些解釋,如果是非阻塞態的話,那其實這個方法是不起作用的,什麼,不信?那來測試下

public class TestClass {
    public static void main(String []agrs){
       Thread thread = new Thread(new Runnable() {
           @Override
           public void run() {
                while (true){
                    System.out.println("run");
                }
           }
       });
       thread.start();
       thread.interrupt();
    }
}
複製程式碼
run
run
run
run
run
run
run
複製程式碼

果然,是不起作用的,那我們再加上阻塞狀態sleep,試一下

public class TestClass {
    public static void main(String []agrs){
       Thread thread = new Thread(new Runnable() {
           @Override
           public void run() {
                while (true){
                    System.out.println("run");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException mE) {
                        mE.printStackTrace();
                        return;
                    }
                }
           }
       });
       thread.start();
       thread.interrupt();
    }
}
複製程式碼
run
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.example.hik.lib.MyClass$1.run(MyClass.java:12)
	at java.lang.Thread.run(Thread.java:748)
複製程式碼

可以看到,在Thread.sleep的方法,丟擲了異常,同時return掉,此時才是停止了執行緒,我們根據捕獲異常實現邏輯,如果無法確定邏輯,那就直接丟擲,由上層去處理。

4.總結

需要注意的一點,wait/notify是Object的方法,其他是Thread的方法,因為每個物件都有內建鎖,主要目的還是理解下執行緒中的一些狀態以及阻塞狀態的本質,希望能夠幫助到大家,如有疑問或者錯誤,歡迎一起討論

請幫頂 / 評論點贊!因為你的鼓勵是我寫作的最大動力!

相關文章