Java 并發編程:本質上只有一種創建線程的方法
在上一篇文章中,我們學習了操作系統中線程的基本概念。那么在 Java 中,我們如何創建和使用線程呢?首先請思考一個問題。創建線程有多少種方法呢?大多數人會說有 2 種、3 種或 4 種。很少有人會說只有 1 種。讓我們看看他們實際指的是什么。最常見的答案是兩種創建線程的方法。讓我們先看看這兩種線程創建方法的代碼。
Thread 類和 Runnable 接口
(1) 繼承 Thread 類:第一種是繼承 Thread 類并重寫 run() 方法:
class SayHelloThread extends Thread {
public void run() {
System.out.println("hello world");
}
}
public class ThreadJavaApp {
public static void main(String[] args) {
SayHelloThread sayHelloThread = new SayHelloThread();
sayHelloThread.start();
}
}
只有在主線程中創建 MyThread 的實例并調用 start() 方法,線程才會啟動。
(2) 實現 Runnable 接口:接下來看看 Runnable 接口:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
實現 Runnable 接口的 run() 方法,在這個方法里可以定義相應的業務邏輯,不過我們還是需要通過 Thread 類來啟動線程。
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Hello Runnable");
}
}
public class RunnableDemo {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
new Thread(myRunnable).start();
}
}
從 Runnable 接口的定義中可以看出,Runnable 是一個函數式接口(JDK 1.8 及以上),這意味著我們可以使用 Java 8 的函數式編程來簡化代碼:
public class RunnableDemo {
public static void main(String[] args) {
// Java 8 函數式編程,可以省略 MyThread 類的定義
new Thread(() -> {
System.out.println("Lambda 表達式實現 Runnable");
}).start();
}
}
Callable、Future 和 FutureTask
一般來說,我們使用 Runnable 和 Thread 來創建一個新線程。然而,它們有一個缺點,即 run 方法沒有返回值。有時我們希望啟動一個線程來執行任務,并且在任務完成后有一個返回值。JDK 為我們提供了 Callable 接口來解決這個問題。
(1) Callable 接口:Callable 與 Runnable 類似,它也是一個只有一個抽象方法的函數式接口。不同之處在于 Callable 提供的方法有返回值并支持泛型。
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
那么 Callable 通常如何使用呢?它是否與 Runnable 接口一樣,傳入 Thread 類呢?讓我們查看 JDK8 的 Java API,發現沒有使用 Callable 作為參數的構造方法。
(2) Future 接口和 FutureTask 類:實際上它提供了 FutureTask 類來完成有返回值的異步計算。FutureTask 實現了 RunnableFuture 接口,而 RunnableFuture 接口同時繼承了 Runnable 接口和 Future 接口,因此可以傳入 Thread(Runable target)。
public interface RunnableFuture<V> extends Runnable, Future<V> {
/**
* Sets this Future to the result of its computation,
* unless it has been cancelled.
*/
void run();
}
Future 接口只有幾個簡單的方法:
public abstract interface Future<V> {
public abstract boolean cancel(boolean paramBoolean);
public abstract boolean isCancelled();
public abstract boolean isDone();
public abstract V get() throws InterruptedException, ExecutionException;
public abstract V get(long paramLong, TimeUnit paramTimeUnit)
throws InterruptedException, ExecutionException, TimeoutException;
}
每個方法的功能如下:
- get():等待計算完成并返回結果。
- get(long paramLong, TimeUnit paramTimeUnit):等待設定的時間。如果在設定時間內計算完成,則返回結果,否則拋出 TimeoutException。
- isDone:如果任務完成,則返回 true。完成可能是由于正常終止、異常或取消。在所有這些情況下,方法都將返回 true。
- isCancelled:如果此任務在正常完成之前被取消,則返回 true。
- cancel:嘗試取消線程的執行。請注意,這是嘗試取消,不一定能成功取消。因為任務可能已經完成、被取消或由于其他一些因素而無法取消,所以取消可能會失敗。布爾類型的返回值表示取消是否成功。參數 paramBoolean 表示是否通過中斷線程來取消線程執行。
有時使用 Callable 而不是 Runnable 是為了能有取消任務的能力。如果使用 Future 只是為了可以取消任務但不提返回結果可以聲明 Future<? >的類型,并將底層任務的結果返回為 null。
你可能會問,為什么要有 FutureTask 類呢?前面說過 Future 只是一個接口,其方法 cancel、get、isDone 等如果自己實現會非常復雜。因此 JDK 為我們提供了 FutureTask 類供我們直接使用。
FutureTask 需要與 Callable 結合使用來完成有返回值的異步計算,這里看一個其使用的簡單示例:
class MyCallable implements Callable<Integer> {
/**
* 計算 1 到 4 的總和
* @return
*/
@Override
public Integer call() {
int res = 0;
for (int i = 0; i < 5; i++) {
res += i;
}
return res;
}
}
publicclass CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 1. 生成 MyCallable 的實例
MyCallable myCallable = new MyCallable();
// 2. 通過 myCallable 創建 FutureTask 對象
FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
// 3. 通過 FutureTask 創建 Thread 對象
Thread t = new Thread(futureTask);
// 4. 啟動線程
t.start();
// 5. 獲取計算結果
Integer res = futureTask.get();
System.out.println(res);
}
}
輸出:
10
為什么只有一種實現線程的方法?
我相信你對這個問題基本上有了答案。無論你是實現 Runnable 接口、實現 Callable 接口還是直接繼承 Thread 類來創建線程,最終都是創建一個 Thread 實例來啟動線程,即 new Thread(),只是創建的形式不同而已!
實際上,線程不僅可以通過上述形式創建,還可以通過內置的工具類(如線程池)來創建,后續文章將單獨介紹。
實現 Runnable 接口優于繼承 Thread 類
要實現一個沒有返回值的線程類,你可以繼承 Thread 類或實現 Runnable 接口,它們之間有什么優缺點呢?
(1) Thread 類的優點:
- 簡單直觀:由于繼承關系,代碼結構相對簡單易懂。
- 線程控制:可以直接使用 Thread 類的方法來控制線程的狀態,如啟動、暫停、停止等。
(2) Thread 類的缺點:
- 單繼承限制:由于 Java 不支持多重繼承,使用 Thread 類限制了類的擴展。
- 代碼耦合:線程類和線程執行邏輯緊密耦合,不利于代碼復用和維護。
(3) Runnable 接口的優點:
- 更好的代碼復用:由于它是一個接口,可以將線程的執行邏輯與其他類分離,以實現代碼復用。
- 靈活性:可以同時實現多個接口,避免單繼承的限制。
- 更好的可擴展性:接口使得在不影響現有代碼的情況下擴展線程功能變得容易。即面向接口編程的原則。
(4) Runnable 接口的缺點:
- 代碼稍微復雜一些:需要創建一個實現 Runnable 接口的類并實現 run() 方法,然后由 Thread 類驅動。
- 沒有線程控制方法:不能直接使用 Thread 類的線程控制方法,需要通過 Thread 對象調用它們。
所以,綜合考慮,通常建議使用 Runnable 接口的實現來創建線程,以獲得更好的代碼可復用性和可擴展性。
Thread 的 start() 方法
在程序中調用 start() 方法后,虛擬機首先為我們創建一個線程,然后等待直到這個線程獲得時間片,才會調用 run() 方法執行具體邏輯。
請注意,start() 方法不能多次調用。第一次調用 start() 方法后,再次調用會拋出 IllegalThreadStateException 異常。
你可以簡單看一下 start() 方法的源代碼。實際上,實際工作是由 start0() 完成的,它是一個本地方法,我添加了一些注釋,以便你更容易理解。
public synchronized void start() {
/**
* 零狀態值對應于狀態 NEW。
*/
if (threadStatus!= 0)
thrownew IllegalThreadStateException();
group.add(this); // 將其所屬的線程組加上該線程
boolean started = false;
try {
start0(); // 本地方法調用實際創建線程的底層方法。
started = true;
} finally {
try {
if (!started) {
group.threadstartFailed(this);
}
} catch (Throwable ignore) {
/* 什么也不做。如果 start0 拋出了 Throwable,那么
* 它將在調用棧中向上傳遞 */
}
}
}
ThreadGroup 的概念將在后續文章中介紹。你可以在這里忽略它。
Thread 類的幾個常用方法
這里我們簡要提及 Thread 類的幾個常用方法,先熟悉一下。后續文章將根據具體使用場景詳細介紹:
- currentThread():靜態方法,返回當前正在執行的線程對象的引用;
- sleep():靜態方法,使當前線程睡眠指定的時間;
- yield():表示當前線程愿意放棄對當前處理器的占用。請注意,即使當前線程調用了 yield() 方法,它仍然有可能繼續運行;
- join():使當前線程等待另一個線程完成執行后再繼續,內部調用是通過 Object 類的 wait 方法實現的;
好了,這次就到這里,下次再見!