並發基礎知識
寫在前面的話
從這篇文章開始就正式進入了並發主題,該主題相關知識較前面的集合主題會比較晦澀難懂,需要不斷回顧、整理,才能構建出自己的知識網絡。
首先推薦一本講並發的好書:JAVA並發編程實戰。這本書非常完整的講解了關於並發的知識點,是一本不可多得的好書,如果有時間一定要多看幾遍。
下面放一張並發主題的思維導圖(受限於網頁大小,請童鞋們自行下載圖片瀏覽):
從這張圖中可以看到並發相關的知識點非常多並且非常雜,我們這個主題不可能把這裏邊所有羅列的知識點全部深入講解一遍,所以我在這裏只能挑選一些相對比較重要的知識點(大家可以認為是主幹知識 : ))進行深入分析,至於剩下的一些枝枝葉葉就需要童鞋們自己去實踐掌握啦。
OK,介紹就到這裏,接下來正式開始知識點剖析,當然首先從基礎知識開始(圖中右下角線程部分)。
並發基礎知識
這部分知識是相對來說比較簡單並且容易理解的,我從這裏開始寫也是為了鞏固童鞋們學習並發知識的信心,讓大家能夠更有動力的去學習。
1.什麽是線程?什麽是進程?它們之間有何聯系?
關於這個問題,我準備用一個形象的例子來描述,這樣比較容易理解:大家都有用過殺毒軟件吧?當你打開殺毒軟件,那麽你就啟動了一個進程。而現代的殺毒軟件功能已不僅僅局限於查殺病毒了,比如還有清理垃圾文件、工具箱、修復漏洞、掃描驅動等等功能,而這一個個功能對應的其實就可以認為是一個個線程在執行任務(當然實際情況可能更復雜)。
從上面這個例子可以得出結論:進程是正在運行的程序的實例,而線程是程序中一個單一的順序控制流程,它是程序執行流的最小單元。一個進程可以擁有許多個線程,這些線程可以共享該進程的全部資源,並且可以並發執行(比如你可以同時進行掃描病毒和修復漏洞的操作)。
2.如何創建並啟動一個線程?
java中創建線程有兩種方式:繼承Thread基類 以及 實現Runnable接口。
繼承Thread基類的方式:
1 class ExtendsThread extends Thread{ 2 3 @Override 4 public void run() { 5 System.out.println("create thread by extends Thread class.........");6 } 7 }
實現Runnable接口的方式:
1 class ImplementsThread implements Runnable{ 2 3 @Override 4 public void run() { 5 System.out.println("create thread by implements Runnable interface........."); 6 } 7 }
創建線程後,就要啟動線程去執行它的工作,兩種創建線程方式有不同的啟動方法:
1 public class CreateThread { 2 3 public static void main(String[] args) { 4 //使用 extends Thread 創建並啟動線程 5 ExtendsThread et = new ExtendsThread(); 6 et.start(); 7 8 //使用 implements Runnable 創建並啟動線程 9 Thread runThread = new Thread(new ImplementsThread()); 10 runThread.start(); 11 12 } 13 }
這裏有一個很古老的面試題:線程創建有哪兩種方式?哪種方式比較好?為什麽?
這個問題的答案要根據具體業務情況來回答,如果確實是非常非常簡單的業務場景,我只需要一個線程就能完成任務,那麽當然是implements Runnable接口的方式比extends Thread類的方式好。
因為大家知道java是單繼承的,如果用extends的方式創建了線程,那麽這個線程類就無法再繼承別的類,而implements的方式則不會有這個問題,相對來說擴展性更好。
但是,現代的企業(尤其是互聯網企業)中,基本不可能看到用這兩種方式來創建線程的,而是使用Executors類提供的許多類型的線程池來創建並管理一組線程,為什麽?
因為在復雜的、對性能和實時性要求非常高的業務場景下,用這兩種創建線程的方式會造成大量資源的消耗,並且線程的上下文切換也會浪費大量的時間,所以java很貼心的為我們提供了線程池,以便於更好的管理許許多多的線程以適應業務需要。
這裏再說一個額外的小知識點,大家有沒有想過,如果我對同一個線程調用兩次start()方法,會出現什麽情況呢?
可以看到java會拋出異常:非正常的線程狀態,在java中是不允許調用兩次同一個線程的start方法的。
3.線程有哪幾種狀態?
既然上面提到了“非正常的線程狀態”,那麽接下來就講一下java中線程的狀態,其實共有6種:
1.NEW 新建狀態。
顧名思義,在此狀態下的線程就是剛剛創建出來的新線程。
2.RUNNABLE 可運行狀態。
調用線程的start()方法並獲取到資源鎖(關於鎖的知識後文會詳細講解)的線程就會進入此狀態。註意,此時的線程可能正在虛擬機中執行任務,也有可能在等待CPU時間分片。
3.BLOCKED 阻塞狀態。
某些代碼可能會被加鎖,而需要執行這些代碼的線程首先需要獲取到鎖對象,如果線程因為沒有獲取到監視器鎖而無法執行任務,則其就處於阻塞狀態。
4.WAITING 等待狀態。
如果線程執行了不帶超時時間的wait方法或者join方法,那麽線程就會讓出執行權,並進入等待狀態。
5.TIME_WAITING 超時等待狀態
如果線程執行了帶超時時間的wait方法或者join方法或者sleep方法,那麽線程就會進入超時等待狀態
6.TERMINATED 終止狀態
無論是響應中斷而強制結束的線程或者是正常執行任務完成後退出的線程都會處於這個狀態。處於終止狀態的線程不具備繼續運行的能力。
線程的六種狀態需要好好理解,這對於後面知識的理解會有一定幫助。
4.如何優雅的結束線程?
最後我們來講一下如何優雅的結束一個線程。有的同學可能會說這還不簡單,Thread相關api中不是有個stop方法嘛,只要調用這個方法線程不就結束了嗎?
這裏我先明確一下結論:stop方法確實有,但是這個方法具有不確定性,jdk1.5及以後這個方法已經被標示為“Deprecated(廢棄)”狀態,即不贊成在代碼中繼續使用該方法。
然而java中並沒有提供能夠立即使一個線程轉變為TERMINATED狀態的方法,取而代之的是提供了一種叫做“中斷”的協調機制,可以使一個線程終止另一個線程當前的工作。
1 /** 2 * 優雅終止線程的方式 3 */ 4 public class TerminateThread { 5 6 public static void main(String[] args){ 7 NeedStopThread nst = new NeedStopThread(); 8 try { 9 nst.start(); 10 Thread.sleep(3000); 11 nst.cancel(); //線程運行3秒後由主線程發出中斷請求 12 } catch (Exception e) { 13 System.out.println("Exception in Main Thread....."); 14 } 15 } 16 } 17 18 class NeedStopThread extends Thread{ 19 20 private AtomicInteger count = new AtomicInteger(); 21 22 @Override 23 public void run() { 24 try { 25 while(!Thread.currentThread().isInterrupted()){ //當前線程未接收到中斷信號 26 System.out.println("i‘m running.........count now is " + count.getAndIncrement()); 27 } 28 } catch (Exception e) { 29 System.out.println("Exception in NeedStop Thread....."); 30 }finally { 31 System.out.println("try to close io or sql resource here........."); 32 } 33 } 34 35 public void cancel(){ 36 interrupt(); //由另一個線程調用發出中斷信號,請求終止當前線程 37 } 38 }
大家可以多次執行這段代碼,會發現結果肯定都是不一樣的,這也證明了中斷操作並不會讓線程立即進入終止狀態,最終進入終止狀態的時間還是由當前線程自己判斷的。
關於線程的基礎知識講解到這裏就結束了。基礎知識大家一定不能小看它,地基要紮穩,樓房才能蓋的好。下篇文章就開始講解線程間的通信和協作的幾種方式,也是非常重要的內容。OK,我們下篇文章見。
並發基礎知識