執行緒詳解
執行緒概述
執行一個音樂播放器播放一首歌,音樂播放器就是一個程序,在程式執行時,既有聲音的輸出,同時還有該歌曲的字幕展示,這就是程序中的兩個執行緒
執行緒與程序
程式進入記憶體就變成了程序,程序就是處於執行中的程式
程序特徵:
- 獨立性:每個程序都有自己的私有地址,一個程序不能直接訪問其他程序
- 動態性:程序有自己的生命週期和不同狀態,而程式不具備
- 併發性:多個程序可以在單個處理器上併發執行,程序之間互不影響
併發: 程序在cpu中切換執行 並行:程序在cpu上一起執行
對於一個CPU而言,它在某個時間點只能執行一個程式,也就是說,只能執行一個程序,
CPU不斷地在這些程序之間輪換執行,雖然CPU在多個程序間輪換執行,但是我們感覺到好像有多個程序在同時進行
執行緒是程序的執行單元,對於絕大多數的應用程式來說,通常僅要求有一個主執行緒,
但也可以在該程序內建立多條順序執行流,這些順序執行流就是執行緒(子執行緒),
每個執行緒也是相互獨立的
執行緒可以擁有自己的堆疊、自己的程式計數器和自己的區域性變數,
但不擁有系統資源,它與父程序的其他執行緒共享該程序所有擁有的全部資源
一個執行緒可以建立和撤銷另一個執行緒,同一個程序中的多個執行緒之間可以併發執行。
多執行緒的優勢
- 程序中的執行緒之間的隔離程度要小。它們共享記憶體、檔案控制代碼和其他的每個執行緒的狀態
- 程序在執行過程中擁有獨立的記憶體單元,而多個執行緒共享記憶體,提高執行效率
- 執行緒共享的環境包括:程序程式碼段、程序的公有資料
- 程序之間不能共享記憶體,但執行緒之間共享記憶體非常容易
- 系統建立程序是需要為該程序重新分配系統資源,但建立執行緒則代價小得多
執行緒的建立與啟動
Java使用Thread類代表執行緒,所有的執行緒物件都必須是Thread類或其子類的例項。
每個執行緒的作用是完成一定的任務,實際上就是執行一段程式程式碼。
Java使用執行緒執行體來代表這段程式程式碼。
Ø 繼承Thread建立執行緒
-
定義Thread類的子類,並重寫該類的run()方法,run()方法的方法體代表執行緒需要完成的任務。因此把run方法稱為執行緒執行體。
- 建立Thread子類的例項,即建立了執行緒物件
- 呼叫執行緒物件的start()方法來啟動該執行緒。
public class Test2 extends Thread{ // 重寫run方法,run方法的方法體就是子執行緒的執行體 @Override public void run() { for (int i = 0; i < 100; i++) {// 繼承Thread類後,從父類繼承的getName方法可以獲取當前執行緒的名稱 System.out.println("執行緒名稱:"+this.getName()+" "+i); } } public static void main(String[] args) { for (int i = 0; i < 100; i++) { // 呼叫Thread的currentThread()方法獲取當前執行緒物件 // 這裡就不能用this來獲取name了 System.out.println("執行緒名稱;"+Thread.currentThread().getName()+"="+i); //建立兩個子執行緒,並執行 if (i == 20){ new Test2().start(); new Test2().start(); } } } }
執行緒是以搶佔式的方式執行的,雖然只建立了兩個執行緒例項,實際上有三個執行緒在執行
(兩個子執行緒,一個主執行緒main)
通過setName(String name)的方式來為執行緒設定名稱,也可以通過getName的方式來得到執行緒的名稱。
在預設情況下,主執行緒的名稱為main,使用者啟動的多執行緒的名稱依次為Thread-0,Thread-1,Thread-3..Thread-n
實現Runnable介面建立執行緒
- 定義Runnable介面的實現類,並重寫該介面的run方法
-
建立Runnable實現類的例項物件,並以此例項物件作為Thread的target來建立Thread類,該Thread物件才是真正的執行緒物件。
- 呼叫執行緒物件的start()方法來啟動該執行緒
public class Test3 implements Runnable{ @Override public void run() { for(int i = 0;i < 100;i++) { // 實現了Runnable介面的類其本質並不是執行緒類,因此沒有getName方法, // 因此需要通過Thread類來獲取當前執行緒,僅僅是一個任務體,仍需交給Thread去執行 System.out.println("執行緒名稱:"+Thread.currentThread().getName()+" "+i); } } public static void main(String[] args) { /*new一個實現了Runnable介面的例項,這個例項不是執行緒物件 * 不能test3.start()來執行子執行緒,執行run方法體 * 實際的執行緒物件要通過new Thread()來獲取,只不過對於實現了Runnable介面的實現類的例項 * 作為引數傳入到new Thread(test3).start()來執行子執行緒 * 意義是讓執行緒物件來執行test3例項的run方法體 * */ Test3 test3 = new Test3(); new Thread(test3).start(); new Thread(test3).start(); } }
又因為Runnable是一個函式式介面,所以可以使用lamda表示式來進行程式碼編寫
public class Test4 { public static void main(String[] args) { /* * 用lamda表示式的寫法,{}裡面寫的就是run方法體 * 將runnable傳入到 new Thread(runnable,"子執行緒1")裡面,就表示了建立了子執行緒 * 並執行run方法體,第二個引數是為子執行緒起名字 * */ Runnable runnable = ()->{ for (int i = 0; i < 100; i++) { System.out.println("執行緒名字:"+Thread.currentThread().getName()+"="+i); } }; Test4 test4 = new Test4(); new Thread(runnable,"子執行緒1").start(); new Thread(runnable,"子執行緒2").start(); } }
通過對比上面兩種建立執行緒的方式,繼承Thread 和 實現Runnable介面,第一種主執行緒和子執行緒
分別執行一遍任務。第二種主執行緒和子執行緒共同完成一個任務。
Ø 使用Callable&Future建立執行緒
在Java 1.5開始,Java提供了Callable介面,該介面實際上可看成是Runnable介面的增強版,Callable介面提供了一個call()的方法可以作為執行緒的執行體,但call()方法比run()方法功能更加強大。
這是因為:
1. call()方法可以有返回值
2. call()方法可以宣告丟擲異常
因此我們可以提供一個Callable物件作為Thread的target,而該執行緒的執行緒執行體就是該Callable物件的call()方法。但是存在以下兩個問題:
1. Callable介面是Java 5提供的一個新的介面而不是Runnable介面的子介面,所以Callable物件不能直接作為Thread類的target目標執行類。
2. call()方法還有一個返回值——call()方法並不是直接呼叫,它是作為執行緒執行體被呼叫的。如何獲取call()方法的返回值?
為了解決以上兩個問題,Java 1.5提供了Future介面來代表Callable接口裡call()方法的返回值,併為Future介面提供了一個FutureTask實現類,該實現類實現了Future介面,並實現了Runable介面——可以作為Thread類的target。
建立並啟動有返回值的執行緒的步驟如下:
- 建立Callable介面的實現類,並實現call()方法,該call()方法將作為執行緒執行體,並且該call()方法有返回值
-
建立Callable實現類的例項,使用FutureTask類來包裝Callable物件,該FutureTask物件封裝了Callable物件的call()方法的返回值。
- 使用FutureTask物件作為Thread物件的target建立並啟動新執行緒
- 呼叫FutureTask物件的get()方法來獲得子執行緒結束後的返回值
import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; /** * 實現Callable介面時指定的泛型為返回值的型別 */ public class Test5 implements Callable<Integer> { /* * 對於實現了Callable介面的類,重寫的call方法就是子執行緒要執行的方法體 * 這個call方法與run方法的區別就是,call有返回值,可以宣告丟擲異常 * * 實現的Callable介面可以看做是Runnable介面的增強版,所以可以提供一個Callable物件作為target傳給執行緒物件 * 但是問題就是,Callable介面不是Runnable介面的子介面,不能直接作為target * call方法有返回值 * * */ @Override public Integer call() throws Exception { for (int i = 0; i < 100; i++) { /*與實現了Runnable介面的run方法相似,也是不能使用this來獲取name*/ System.out.println("當前執行緒名稱:"+Thread.currentThread().getName()+" "+i); Thread.sleep(200); } return 100; } public static void main(String[] args) throws ExecutionException, InterruptedException { /*建立Callable物件,因為當前類實現了Callable介面 多型*/ Callable<Integer> callable = new Test5(); // 建立FutureTask物件,並將call物件封裝在FutureTask內部,FutureTask的泛型為Callable FutureTask<Integer> futureTask = new FutureTask<>(callable); /*建立執行緒物件*/ new Thread(futureTask).start(); // 獲取執行緒結束後的返回值 System.out.println("執行緒執行結束後的返回值:"+futureTask.get()); } }
Callable介面是一個函式式介面,所以可以用lamda表示式寫法
import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; public class Test6 { public static void main(String[] args) throws Exception { /* * Callable介面也是一個函式式介面,所以可以用lamda表示式寫法 * {} 裡面寫的就是call的方法體 * */ Callable<Integer> callable = ()->{ for (int i = 0; i < 100; i++) { System.out.println("執行緒名稱:"+Thread.currentThread().getName()+"="+i); } return 100; }; // 建立FutureTask物件,並將call物件封裝在FutureTask內部,FutureTask的泛型為Callable FutureTask<Integer> futureTask = new FutureTask<>(callable); /*建立執行緒物件 並將FutureTask封裝好的Callable物件傳入執行緒物件中*/ new Thread(futureTask).start(); // 獲取執行緒結束後的返回值 System.out.println("執行緒執行結束後的返回值:"+futureTask.get()); } }
Ø 建立執行緒的三種方式比較
通過繼承Thread類或實現Runnable、Callable介面都可以實現多執行緒,不過實現Runnable介面與實現Callable介面的方式基本相同,只是Callable接口裡定義的方法有返回值,可以宣告丟擲異常,並且Callable需要FutureTask來進行封裝成Thread可識別的target目標。因此可以將實現Runnable介面和實現Callable介面歸納為一種方式。這種方式與繼承Thread方式之間的主要差別如下
執行緒的宣告週期
當執行緒被建立並啟動後,並不是一啟動就進入了執行狀態,也不是一直處於執行狀態,
線上程的生命週期中,它要經歷新建、就緒、執行、阻塞和死亡5種狀態。
尤其是當執行緒啟動以後,它不可能一直佔用CPU獨自執行,所以CPU需要在多條執行緒之間切換,於是執行緒狀態也會在執行、阻塞之間切換。
新建狀態:當new了一個執行緒之後,該執行緒就處於新建狀態,此時它和其他的Java物件一樣僅僅由Java虛擬機器為其分配記憶體,並初始化其他成員變數的值
就緒狀態:當執行緒物件呼叫了start方法之後,該執行緒就處於就緒狀態,Java虛擬機器會為這個執行緒物件建立方法呼叫棧和程式計數器,處於這個狀態中的執行緒並沒有開始執行,只是表示該執行緒可以運行了。至於什麼時候開始執行,則取決於JVM裡的執行緒排程器的排程。
執行狀態:處於就緒狀態的執行緒獲得了CPU,開始執行run方法的執行緒執行體,則該執行緒就處於執行狀態
阻塞狀態:
- 執行緒呼叫sleep()方法主動放棄所佔用的處理器資源
- 執行緒呼叫了一個阻塞式IO方法,在該方法返回之前,該執行緒被阻塞
- 執行緒試圖獲得一個同步監視器,但該同步監視器正被其他執行緒所持有
-
執行緒在等待某個通知(notify)
- 程式呼叫了執行緒的suspend()方法將該執行緒掛起。但這個方法容易導致死鎖,所以應該儘量避免使用該方法
死亡狀態:
- run()或call()方法執行完成,執行緒正常結束
- 執行緒丟擲一個未捕獲的Exception或者直接Error錯誤
- 直接呼叫該執行緒的stop()方法來結束該執行緒——該方法容易導致死鎖,通常不推薦使用
當主執行緒結束時,其他執行緒不受任何影響,並不會隨之結束。一旦子執行緒啟動之後,它就擁有和主執行緒相同的地位
不要試圖對一個已經死亡的執行緒呼叫start()方法使它重新啟動,死亡就是死亡,該執行緒不可以再次作為執行緒執行。
控制執行緒