Thread執行緒的基礎知識及常見疑惑點

張曾經發表於2019-05-25

引言

    相信各位道友在平時工作中已經很少直接用到Thread執行緒類了,現在大多是通過執行緒池或者一些多執行緒框架來操作執行緒任務,但我覺得還是有必要了解清楚Thread執行緒類中各種方法的含義,瞭解了底層才能更好的理解框架、應用框架。下面我就將Thread執行緒的相關基礎點總結一二,以供觀瞻。

正文

1、Thread執行緒的狀態

    根據《深入理解Java虛擬機器》一書的講述,Java語言定義了五種執行緒狀態,分別為:建立(new)、執行(Runnable)、等待(waiting)、阻塞(blocked)、結束(terminated)。而且規定,在某一個時間點,每個執行緒能且只能處於其中的一種狀態。

其中,執行狀態又包括就緒(Ready)跟正在執行(Running),區別就是是否獲得了CPU的執行時間。

對於等待跟阻塞狀態,需要著重說明一下,因為此處極易搞錯,而且也是面試常被問到的點。等待狀態,一般由Object.wait()、Thread.sleep()、Thread.join()、LockSupport.park()等方法以及這些方法帶時間控制的同類方法實現執行緒的等待。而阻塞狀態,一般是由於當前執行緒還未獲取到獨佔鎖且正在等待獲取,此時稱為阻塞。可以將等待看做主動的執行緒暫停執行,以為需要呼叫特定的方法執行緒才會等待;而阻塞可以看做是被動的執行緒暫定執行,因為執行緒在等著獲取獨佔鎖。

2、Thread執行緒的相關方法

    start()方法/run()方法:有時在面試的時候,面試官會問到呼叫執行緒的start方法跟直接呼叫run方法有什麼區別?雖然有的道友看到這裡會覺得問這種問題的面試官有點很沒必要,但我還是說一下。呼叫start方法後,最終會呼叫Thread類中的一個本地方法start0,這個方法可以新建一個執行緒來執行你的run方法,而呼叫run方法後只是在當前執行緒上執行你的run方法,並沒有新執行緒參與。

    wait()方法/sleep()方法:請注意,這裡很多人都會記錯,wait方法以及跟它配套的notify/notifyAll方法,是位於頂級父類Object下的,而其他操作執行緒的方法都在Thread執行緒類下。為什麼要將wait方法放在Object下呢?其實這是由wait/notify方法的實現原理決定的。wait方法呼叫了之後,會釋放鎖,並讓當前執行緒等待,而對於java的原生鎖synchronized,是隸屬於一個特定物件的監視器monitor的,那這個釋放的是鎖誰的鎖?不能是別人的,只能是呼叫wait方法的那個物件的。而這個鎖是哪裡來的?要釋放鎖,肯定之前加過鎖,在哪裡加的呢?只能是在synchronized塊中給這個物件加的,所以這也解釋了為什麼wait/notify方法一直要跟synchronized一起用,因為它倆就是通過操作物件的鎖實現的等待和喚醒。相比而言sleep方法單純很多,它只是讓當前執行緒睡眠一段時間,並不會涉及到對鎖的操作,所以直接放在Thread類中就行。對於wait跟notify的演示如下:

 1 public static void main(String[] args) throws InterruptedException {
 2         Object obj = new Object();
 3         Thread thread = new Thread(new Runnable() {
 4             @Override
 5             public void run() {
 6                 synchronized (obj) {
 7                     try {
 8                         System.out.println("thread獲取到鎖,觸發wait");
 9                         obj.wait();
10                         System.out.println("wait over");
11                     } catch (InterruptedException e) {
12                         e.printStackTrace();
13                     }
14                 }
15             }
16         });
17         Thread thread1 = new Thread(new Runnable() {
18             @Override
19             public void run() {
20                 synchronized (obj) {
21                     try {
22                         System.out.println("thread1獲取到鎖");
23                         Thread.sleep(1000);
24                         System.out.println("1秒後喚醒");
25                         obj.notify();
26                     } catch (Exception e) {
27                         e.printStackTrace();
28                     }
29                     System.out.println("notify over");
30                 }
31 
32             }
33         });
34         thread.start();
35         thread1.start();
36     }

執行結果為:

thread獲取到鎖,觸發wait
thread1獲取到鎖
1秒後喚醒
notify over
wait over

  LockSupport.park():另外還有JUC包中的park方法讓當前執行緒等待。此方法是使用CAS實現的執行緒等待,不會釋放鎖。而park/unpark方法比wait/notify這一對好的地方在於,前者可以先unpark在park,這是執行緒仍然會繼續執行;而對於wait/notify,則需要通過程式控制執行順序,一定要先wait在notify/notifyAll,否則順序反了執行緒就會一直等待下去,由此悲劇誕生...  比如講上述wait/notify的程式碼34行35行調換一下順序,執行結果如下所示:

thread1獲取到鎖
1秒後喚醒
notify over
thread獲取到鎖,觸發wait

彷彿雲天明對程心那一千八百萬年的等待

    join()/yield():對於Thread下的這兩個方法,之所以放在一起講解,就是因為這兩個方法平時比較少用到,屬於閒雲野鶴的存在。

yield()方法是讓當前執行緒讓步,讓步的意思就是放棄執行權,即當前執行緒會從上述說的執行狀態runnable中的running狀態進入ready就緒狀態,但是虛擬機器不保證當前執行緒執行了yield方法後不會緊接著再次進去running狀態,因為可能CPU分配執行時間時又分給了當前執行緒。所以這個方法其實一般也沒啥用,因為效果不穩定。

join()方法是將呼叫join的執行緒插入當前執行緒的執行過程中,即讓當前執行緒等待,先執行完呼叫join的執行緒,再繼續執行當前執行緒。注意join方法不會釋放鎖。join的演示程式碼如下:

 1 public class RunnableThread implements Runnable{
 2     @Override
 3     public void run() {
 4         System.out.println("runnable run");
 5         try {
 6             System.out.println("開始睡眠");
 7             Thread.sleep(5000);
 8             System.out.println("睡了5秒");
 9         } catch (Exception e) {
10             System.out.println("runnable exception:" + e);
11         }
12     }
13 
14     public static void main(String[] args) throws InterruptedException {
15         Object obj = new Object();
16         Thread thread = new Thread(new RunnableThread());
17         thread.start();
18         thread.join();
19         System.out.println("end");
20     }
21 }

執行結果為:

runnable run
開始睡眠
睡了5秒
end

結束語

    這次先到這裡,上述說的東西,雖然很小,而且實際中不會直接用到,但是對於我們理解執行緒的執行機制、理解多執行緒框架都有好處,所以還是有必要在自己的學習地圖上理解清楚。其實執行緒還有一個很重要的點就是執行緒的中斷,多執行緒框架或者JUC包的原始碼中都會涉及到對執行緒中斷的處理以及響應,這一塊我會在後面梳理清楚了之後專門整理出來。最近覺得學習進入了停滯期,有點不知道從何下手,覺得需要學的東西太多。在這裡,想跟各位道友討教一下,一個資質普通的開發者,如何才能將自己的實力提升到一個比較高的層次(比如阿里的P6P7及以上?)歡迎留言賜教,在此不勝感激!

 

相關文章