Thread執行緒的基礎知識及常見疑惑點
引言
相信各位道友在平時工作中已經很少直接用到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及以上?)歡迎留言賜教,在此不勝感激!
&n