Java中的線程有三種建立方式:繼承Thread類、實作Runnable接口和實作Callable接口。下面我們一一進行介紹。
一、繼承Thread類
使用該方法建立線程有如下三步:
1. 自定義線程類繼承Thread類;
2. 重寫該類中的run()方法,編寫線程執行體;
3. 建立線程對象,調用start()方法啟動線程。
例如,我們建立3個線程,分别去尋找指定區間内的素數。
public class Main {
public static void main(String[] args) {
EnumeratePrimeNumber task1 = new EnumeratePrimeNumber(10000, 11000);
EnumeratePrimeNumber task2 = new EnumeratePrimeNumber(20000, 21000);
EnumeratePrimeNumber task3 = new EnumeratePrimeNumber(30000, 31000);
task1.start();
task2.start();
task3.start();
}
}
class EnumeratePrimeNumber extends Thread {
private int start;
private int end;
public EnumeratePrimeNumber() {}
public EnumeratePrimeNumber(int start, int end) {
this.start = start;
this.end = end;
}
@Override
public void run() {
for (int i = start; i < end; ++i) {
boolean isPrime = true;
for (int j = 2; j * j <= i; ++j) {
if (i % j == 0) {
isPrime = false;
break;
}
}
if (isPrime)
System.out.println(i);
}
}
}
該程式的核心點有:
- 語句
定義了任務類EnumeratePrimeNumber,它繼承了Thread類。class EnumeratePrimeNumber extends Thread
-
@Override public void run() {...}
重寫了Thread類裡的run()方法,進而可以尋找素數。
-
EnumeratePrimeNumber task1 = new EnumeratePrimeNumber(10000, 11000); EnumeratePrimeNumber task2 = new EnumeratePrimeNumber(20000, 21000); EnumeratePrimeNumber task3 = new EnumeratePrimeNumber(30000, 31000);
分别建立了3個線程對象,分别尋找10000~11000、20000~21000和30000~31000間的素數。
-
task1.start(); task2.start(); task3.start();
調用start()方法啟動這3個線程。
程式的運作結果如下所示:
10007
10009
30011
20011
30013
10037
10039
10061
30029
20021
30047
10067
10069
......
可以看到,程式的執行結果并非是按照10000~11000、20000~21000和30000~31000三個區間依次輸出的,而是在這三個區間之間來回切換,但是每個區間内找到的素數依然還是有序的。
二、實作Runnable接口
1. 自定義線程類實作Runnable接口;
3. 建立自定義線程類的對象,接着建立Thread線程類對象,并将自定義線程類的對象作為Thread線程類構造方法的傳入參數,最後調用Thread線程類對象的start()方法啟動線程。
具體的步驟其實和第一種線程建立方式類似,隻是第一步不再是繼承而是實作,第三步不是直接調用自定義線程類對象的start方法,而是通過Thread線程類對象進行代理。
依然以上面尋找素數的問題為例,使用Runnable接口的實作程式如下:
public class Main {
public static void main(String[] args) {
EnumeratePrimeNumber task1 = new EnumeratePrimeNumber(10000, 11000);
EnumeratePrimeNumber task2 = new EnumeratePrimeNumber(20000, 21000);
EnumeratePrimeNumber task3 = new EnumeratePrimeNumber(30000, 31000);
// can also be
// Thread t1 = new Thread(task1);
// t1.start();
new Thread(task1).start();
new Thread(task2).start();
new Thread(task3).start();
}
}
class EnumeratePrimeNumber implements Runnable {
private int start;
private int end;
public EnumeratePrimeNumber() {}
public EnumeratePrimeNumber(int start, int end) {
this.start = start;
this.end = end;
}
@Override
public void run() {
for (int i = start; i < end; ++i) {
boolean isPrime = true;
for (int j = 2; j * j <= i; ++j) {
if (i % j == 0) {
isPrime = false;
break;
}
}
if (isPrime)
System.out.println(i);
}
}
}
上面代碼相比于之前的,最大的差別有:
- 自定義任務類時不再繼承Thread類,而是實作Runnable接口:
class EnumeratePrimeNumber implements Runnable
- 在啟動線程時,需要先建立任務類的執行個體,并在建立Thread對象時作為參數傳遞,再調用start()方法啟動:
new Thread(task1).start();
由于Java隻支援單繼承,是以在實踐中更推薦使用實作Runnable接口建立線程。除此之外,這樣的方式還支援同一對象被多個線程所使用。例如,我們可以編寫程式模拟“搶票”——由多個線程去使用同一資源。
public class Main {
public static void main(String[] args) {
SnapUp race = new SnapUp();
new Thread(race, "Alice").start();
new Thread(race, "Bob").start();
new Thread(race, "Cathy").start();
}
}
class SnapUp implements Runnable {
private static int numberOfTicket = 10;
@Override
public void run() {
while (true) {
if (numberOfTicket > 0) {
System.out.println(Thread.currentThread().getName() + " gets " + (11 - numberOfTicket) + " ticket");
numberOfTicket--;
} else {
break;
}
}
}
}
上述程式安排了3個線程去模拟3個人,搶購10張票,并輸出搶購結果。運作結果如下:
Alice gets 1 ticket
Alice gets 2 ticket
Alice gets 3 ticket
Bob gets 1 ticket
Alice gets 4 ticket
Alice gets 6 ticket
Alice gets 7 ticket
Bob gets 5 ticket
Bob gets 9 ticket
Bob gets 10 ticket
Cathy gets 8 ticket
Alice gets 8 ticket
非常驚訝的是,第1張票被Alice和Bob兩個人都搶到了,第8張票被Cathy和Alice兩個人都搶到了,這顯然是不正确的。專業上将其稱之為多線程通路導緻的資料不一緻。
最後總結一下使用實作Runnable接口建立線程的好處:
1. 避免了Thread類單繼承的局限,更加靈活友善;
2. 友善同一個對象被多個線程使用。
三、實作Callable接口
有時候我們希望傳回每個線程的執行結果,或者希望線程可以抛出異常,那麼我們可以使用實作Callable接口建立線程。
實作Callable接口來建立線程的步驟如下:
1. 自定義線程類實作Callable接口;
2. 重寫該類中的call()方法,編寫線程執行體,給出傳回值并抛出異常;
3. 建立自定義線程類的對象,比如
Task t = new Task();
4. 建立執行服務,比如
ExecuteService es = Executors.newFixedThreadPool(3); // 這裡建立了大小為3的定長線程池
5. 送出執行,比如
Future<Integer> futureResult = es.submit(t); // 這裡的傳回值為Integer類
6. 擷取結果,比如
Integer result = futureResult.get();
7. 關閉服務
es.shutdownNow();
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Summation task1 = new Summation(1, 10000);
Summation task2 = new Summation(10000, 20000);
Summation task3 = new Summation(20000, 30000);
ExecutorService es = Executors.newFixedThreadPool(3);
Future<Double> futureSum1 = es.submit(task1);
Future<Double> futureSum2 = es.submit(task2);
Future<Double> futureSum3 = es.submit(task3);
double sum1 = futureSum1.get();
double sum2 = futureSum2.get();
double sum3 = futureSum3.get();
System.out.println("Sum = " + (sum1 + sum2 + sum3));
es.shutdown();
}
}
class Summation implements Callable<Double> {
private int start;
private int end;
public Summation() {}
public Summation(int start, int end) {
this.start = start;
this.end = end;
}
@Override
public Double call() throws ArithmeticException {
double sum = 0.0;
for (int i = start; i < end; ++i) {
if (i == 0)
throw new ArithmeticException();
sum += 1.0 / i;
}
System.out.println(Thread.currentThread().getName() + ": " + sum);
return sum;
}
}
pool-1-thread-2: 0.6931721811849485
pool-1-thread-3: 0.40547344155723775
pool-1-thread-1: 9.787506036044348
Sum = 10.886151658786535