Java建立執行緒的三種方式以及優劣對比
Java使用Thread類代表執行緒,所有的執行緒物件都必須是Thread類或者其子類例項。每個執行緒的作用是完成一定的任務,實際上是執行一段程式流
#1. 繼承Thread類建立執行緒類 通過繼承Thread類來建立並啟動多執行緒的步驟如下:
- 定義Thread類的子類,並重寫該類的run()方法。
- 建立子類的例項,即建立了執行緒物件。
- 呼叫執行緒物件的start()方法來啟動該執行緒。
考慮下面這個示例:
public class ThreadTest extends Thread{
//重寫run()方法,run()方法的方法體是執行緒執行體
public void run(){
for(int i=0;i<5;i++){
//使用執行緒的getName()方法可以直接獲取當前執行緒的名稱
System.out.println(this.getName() + " " + i);
}
}
public static void main(String[] args){
//輸出Java程式執行時預設執行的主執行緒名稱
System.out.println(Thread.currentThread().getName( ));
//建立第一個執行緒並開始執行
new ThreadTest().start();
//建立第二個執行緒並開始執行
new ThreadTest().start();
}
}
輸出的示例如下: 因為多執行緒程式在執行的時候,多個執行緒在一個CPU上快速輪換執行,所以可能得到的輸出結果並不相同。
注意:
-
Java程式執行時有預設的主執行緒,它的方法體就是main()方法的方法體。
-
使用繼承Thread類的方法來建立執行緒類時,多個執行緒之間無法共享執行緒類的例項變數。 #2.實現Runnable介面來建立執行緒類 實現Runnable介面來建立並啟動多執行緒的步驟如下:
-
定義Runnable介面的實現類,並重寫run()方法。
-
建立Runnable實現類的例項,並將它作為Target傳入Thread類的構造器以建立Thread物件。
-
呼叫執行緒物件的start()方法來啟動執行緒。
考慮下面這個示例:
public class ThreadTest implements Runnable{
private int i;
//重寫run()方法,run()方法的方法體是執行緒執行體
public void run(){
//實現Runnable介面時,只能使用如下方法獲取執行緒名
System.out.println(Thread.currentThread().getName() + " " + i);
i++;
}
public static void main(String[] args){
ThreadTest tt = new ThreadTest();
//建立第一個執行緒並開始執行
//輸出 新執行緒1 0
new Thread(tt,"新執行緒1").start();
//建立第二個執行緒並開始執行
//輸出 新執行緒2 1
new Thread(tt,"新執行緒2").start();
//使用Lambda表示式建立Runnable物件
new Thread(()->{
System.out.print("AmosH");
System.out.println("'s blog");
}).start();
}
}
這兩個建立執行緒類的方法之間的區別是:前者直接建立的Thread子類即可代表執行緒物件;後者建立的Runnable物件只能作為執行緒物件的target。
注意:
使用這種方法,程式建立的Runnable物件只是執行緒的target,而多個執行緒可以共享同一個target,所以多個執行緒可以共享同一個執行緒類的例項變數。 #3.使用Callable和Future建立執行緒 當使用實現Runnable介面以建立執行緒時,我們只能把run()方法包裝成為執行緒執行體,那麼是否可以把任意方法都包裝成執行緒執行體呢?
答案是不行。但是從Java5開始,Java提供了Callable介面,該介面提供了一個可以作為執行緒執行體的call()方法,同時該方法可以有返回值而且可以丟擲錯誤。
但是Callable介面不能直接作為Thread的target來使用,Java 5中提供了Future介面來代表Callable介面中的call()方法的返回值,並且為Future介面提供了一個FutureTask實現類,該實現類實現了Future介面,並實現了Runnable介面——可以作為Thread類的target。
同時在Future介面中還定義瞭如下幾個公共方法用來控制和它關聯的Callable任務。
- boolean cancel(boolean mayInterruptIfRunning):試圖取消該Future關聯的Callable任務。
- V get():獲取關聯的任務的返回值。呼叫該方法將導致程式阻塞,必須等到子執行緒結束後才會得到返回值。
- V get(long timeout,TimeUnit unit):獲取關聯的任務的返回值。該方法最多讓程式阻塞timeout和unit指定時間,如果經過指定時間後Callable任務仍然沒有返回值,則將丟擲TimeoutException異常。
- boolean isCancelled():如果在Callable任務完成之前被取消,則返回true
- boolean isDone():如果任務已經完成,則返回true
Callable介面有泛型限制,介面中的泛型形參型別必須和call()方法返回值型別相同。而且Callable介面是函式式介面,因此可以使用Lambda表示式建立Callable物件。
使用該方法建立並啟動執行緒的步驟類似於實現Runnable介面來建立執行緒,考慮下面這個例子:
import java.util.concurrent.FutureTask;
public class ThreadTest {
public static void main(String[] args){
//使用FutureTask來包裝Callable物件
//使用Lambda表示式來建立Callable<Integer>物件
FutureTask<Integer> task = new FutureTask<>(()->{
System.out.println(Thread.currentThread().getName() + " " + "開始執行任務!");
return 0;
});
//實質還是以Callable物件來建立並啟動執行緒
//輸出 新執行緒 開始執行任務!
new Thread(task,"新執行緒").start();
try{
//獲取執行緒的返回值
//輸出 0
System.out.print(task.get());
}catch (Exception ex){
ex.printStackTrace();
}
}
}
#建立執行緒的三種方式的對比: 採用Runnable和Callable介面的方式建立多執行緒的優缺點:
-
執行緒類只實現了Runnable介面或者Callable介面,還可以繼承其他類。
-
多個執行緒可以共享一個target物件,非常適合多個相同執行緒來處理同一份資源的情況,從而可以將CPU、程式碼和資料分開,形成清晰的模型,體現了面向物件的程式設計思想。
-
程式設計稍微複雜些。 採用繼承Thread類的方式建立多執行緒的優缺點:
-
執行緒類已經繼承了Thread類,不能再繼承其他類。
-
程式設計比較簡單。
綜上,一般推薦使用Runnable和Callable介面的方式建立多執行緒.