0. 前言
Java 為了實現(xiàn)跨平臺,在語言層面上實現(xiàn)了多線程。我們只需要熟悉 Java 這一套多線程機制就行了,比 C/C++ 要容易多了。
1. 定義任務(wù)
我們編寫程序,最終是為了完成特定的任務(wù)。為了更有效的利用系統(tǒng)資源,我們把任務(wù)合理地劃分成多個子任務(wù),放到多個線程中來執(zhí)行。所以,首先我們需要一種描述任務(wù)的方式。在 Java 中,一般我們都用 Runable 接口來定義任務(wù)。
public interface Runnable {
// 在run方法中定義任務(wù)
public void run();
}
想要定義任務(wù),只需要實現(xiàn) Runable 接口,然后在 run() 方法中寫上執(zhí)行步驟。請注意,Runable 只是定義了一個任務(wù),本身不會去啟動一個新線程來執(zhí)行。看下面的例子。可以看到,在外面直接打印的線程名和在 Runable 的 run() 方法中打印的線程名是相同的。
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName());
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
};
runnable.run();
}
輸出結(jié)果:
main
main
2. Thread類
? 要讓任務(wù)在新的線程執(zhí)行,最直接的方法是用它來創(chuàng)建一個 Thread 類。這里用 Thinking in Java 書上的例子來展示 Thread 類的使用。
/**
* 顯示發(fā)射之前的倒計時
*/
public class LiftOff implements Runnable {
private static int sTaskCount = 0;
private final int mId = sTaskCount++;
protected int mCountDown = 10;
public LiftOff() {
}
public LiftOff(int countDown) {
mCountDown = countDown;
}
private String status() {
return "#" + mId + "(" +
((mCountDown > 0) ? mCountDown : "Liftoff!") + "), ";
}
@Override
public void run() {
while (mCountDown-- > 0) {
System.out.print(status());
Thread.yield(); // Thread.yield() 是對線程調(diào)度器的一種建議,表示當(dāng)前線程準(zhǔn)備讓出處理器
}
}
}
LiftOff 任務(wù)會顯示發(fā)射前的倒計時。注意在 run() 方法中調(diào)用的 Thread.yield() 方法。這個方法的作用是對線程調(diào)度器的一種建議,表示當(dāng)前線程可以讓出處理器。當(dāng)然,線程調(diào)度器不一定會真的切換執(zhí)行線程。LifiOff 任務(wù)整個執(zhí)行時間實際上很短,如果不使用 Thread.yield() 很可能直到任務(wù)執(zhí)行完成線程調(diào)度器才會切換新的線程,不利于觀察多線程的效果。
public class MoreBasicThreads {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(new LiftOff()).start();
}
System.out.println("Waiting for Liftoff");
}
}
輸出結(jié)果:
#0(9), #1(9), #2(9), #3(9), #0(8), Waiting for Liftoff
#4(9), #1(8), #2(8), #3(8), #0(7), #4(8), #1(7), #2(7), #3(7), #0(6), #4(7), #1(6), #2(6), #3(6), #0(5), #4(6), #1(5), #2(5), #3(5), #0(4), #4(5), #1(4), #2(4), #3(4), #0(3), #4(4), #1(3), #2(3), #3(3), #0(2), #4(3), #1(2), #2(2), #3(2), #0(1), #4(2), #1(1), #2(1), #3(1), #0(Liftoff!), #4(1), #1(Liftoff!), #2(Liftoff!), #3(Liftoff!), #4(Liftoff!),
? Thread的構(gòu)造器接收 Runable 對象,并在調(diào)用 start() 方法之后啟動新的線程去執(zhí)行 Runable中的 run() 方法。輸出結(jié)果很有意思。我們啟動了5個發(fā)射前的倒計時任務(wù),“ Waiting for Liftoff ” 在倒計時沒完成之前就輸出了,這證明現(xiàn)在的任務(wù)確實是在新的線程執(zhí)行的。各個任務(wù)的倒計時混雜在一起,說明不同任務(wù)的執(zhí)行線程在被不斷的換進(jìn)換出。
3. 使用Executor
java.util.concurrent 包中的執(zhí)行器( Executor ),可以幫我們管理Thread對象。 Executor 是一個接口,只有一個方法,就是 execute 。當(dāng)我們把一個 Runable 交給 Executor 去執(zhí)行,它可能會啟動一個新的線程、或者從線程池中選擇一個線程、甚至直接使用當(dāng)前線程。但是,這些我們都不需要關(guān)心,我們只需要選擇合適的 Executor 的實現(xiàn),然后把任務(wù)扔給它去執(zhí)行就好了。
public interface Executor {
/**
* Executes the given command at some time in the future. The command
* may execute in a new thread, in a pooled thread, or in the calling
* thread, at the discretion of the {@code Executor} implementation.
*
* @param command the runnable task
* @throws RejectedExecutionException if this task cannot be
* accepted for execution
* @throws NullPointerException if command is null
*/
void execute(Runnable command);
}
先來看一個具體的使用示例。在這個示例中,我們通過 Executors 來創(chuàng)建了一個 線程池 CachedThreadPool。并通過這個線程池來執(zhí)行5個發(fā)射前的倒計時任務(wù)。
public class CachedThreadPool {
public static void main(String[] args) {
ExecutorService exectorService = Executors.newCachedThreadPool();
for (int i=0; i<5;++i) {
exectorService.execute(new LiftOff());
}
exectorService.shutdown();
}
}
在上面的示例中有幾個需要解釋的概念:
- Executors:一個工廠和工具類。
- ExecutorService:有生命周期的 Executor 。也是一個接口,繼承于 Executor 。
- CachedThreadPool:線程池。當(dāng)新任務(wù)過來,會首先找池中有沒有可用的線程,沒有才新建線程。
在 Executors 中還定義了另外三種線程池:FixedThreadPool 、SingleThreadPool 、 ScheduledThreadPool (也提供了單線程的 ScheduledThreadPool )。FixedThreadPool 線程數(shù)量是穩(wěn)定的,線程創(chuàng)建后不會銷毀,達(dá)到設(shè)定的數(shù)量后,不再創(chuàng)建新線程。SingleThreadExecutor 是只能有一個線程的線程池。而 ScheduledThreadPool 可以定時執(zhí)行任務(wù)。現(xiàn)在把上面的示例中的 CachedThreadPool 換成 FixedThreadPool ,最大線程數(shù)量為3。
public class FixedThreadPool {
public static void main(String[] args) {
ExecutorService exector = Executors.newFixedThreadPool(3);
for (int i=0; i<5;++i) {
exector.execute(new LiftOff());
}
exector.shutdown();
}
}
輸出結(jié)果:
#1(9), #2(9), #0(9), #1(8), #2(8), #0(8), #1(7), #2(7), #0(7), #1(6), #0(6), #2(6), #1(5), #0(5), #2(5), #1(4), #0(4), #2(4), #1(3), #0(3), #2(3), #0(2), #1(2), #0(1), #2(2), #0(Liftoff!), #1(1), #2(1), #1(Liftoff!), #2(Liftoff!), #3(9), #4(9), #3(8), #4(8), #3(7), #4(7), #3(6), #4(6), #3(5), #4(5), #3(4), #4(4), #3(3), #4(3), #3(2), #4(2), #3(1), #4(1), #3(Liftoff!), #4(Liftoff!),
從輸出結(jié)果可以看到,只有三個任務(wù)在同時執(zhí)行。后面兩個任務(wù)等前面的任務(wù)執(zhí)行完成了,才開始執(zhí)行。
對線程池的進(jìn)一步研究
來看一下 Executors 中這四種線程池是怎么創(chuàng)建的。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public class ScheduledThreadPoolExecutor
extends ThreadPoolExecutor
implements ScheduledExecutorService {
...
}
前面三種線程池,都是直接創(chuàng)建了 ThreadPoolExecutor 類的對象。ScheduledThreadPool 因為要實現(xiàn)定時功能,創(chuàng)建的是 ScheduledThreadPoolExecutor 類的對象。但 ScheduledThreadPoolExecutor 也是繼承自ThreadPoolExecutor 。所以我們主要關(guān)注一下 ThreadPoolExecutor 。下面這個構(gòu)造方法是參數(shù)最全的一個。
/**
* Creates a new {@code ThreadPoolExecutor} with the given initial
* parameters.
*
* @param corePoolSize 線程池中維持的線程數(shù)量。
* 當(dāng)線程數(shù)量不超過這個數(shù)時,即使線程處于空閑狀態(tài)也不會被銷毀,會一直等待任務(wù)到來。
* 但是如果設(shè)置 allowCoreThreadTimeOut 為 true,corePoolSize 就不再有效了。
* @param maximumPoolSize 線程池中線程的最大數(shù)量。
* @param keepAliveTime 當(dāng)線程數(shù)量超過了 corePoolSize 時,多余的線程銷毀前等待的時間。
* @param unit keepAliveTime 的時間單位
* @param workQueue 用來管理待執(zhí)行任務(wù)的隊列。
* @param threadFactory 創(chuàng)建線程的工廠。
* @param handler RejectedExecutionHandler 接口的實現(xiàn)對象。用于處理任務(wù)被拒絕執(zhí)行的情況。
* 被拒絕的原因可能是所有線程正在執(zhí)行任務(wù)而任務(wù)隊列容量又滿了
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
...
}
理解了這些參數(shù),就很容易理解 Executors 中創(chuàng)建的幾種線程池。當(dāng)這幾種線程池都不能滿足需求的時候,我們直接可以通過 ThreadPoolExecutor 的構(gòu)造方法來創(chuàng)建一個合適的線程池。那么,ThreadPoolExecutor 是怎么調(diào)度線程來執(zhí)行任務(wù)的呢?
從 execute() 方法入手去理解。其中 ctl 只是一個原子操作的 int 型(AtomicInteger類)變量,但可以同時保存線程池狀態(tài)和線程數(shù)量。我在另一篇文章中專門分析了這個 ctl 的實現(xiàn)。
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
如果線程數(shù)量小于核心線程數(shù)量,就創(chuàng)建新的線程來執(zhí)行任務(wù);不然就添加到任務(wù)隊列中;如果添加到任務(wù)隊列失敗,就創(chuàng)建新的線程來執(zhí)行;如果創(chuàng)建線程再失敗(可能是線程池不再是RUNNING狀態(tài),或者線程數(shù)量已經(jīng)達(dá)到了最大線程數(shù)量),就只能拒絕任務(wù)了。
上面說的線程,實際上都通過 Worker 來管理,每個 Worker 對象持有一個線程。而 Woker 實現(xiàn)了 Runable 接口,會在自己的管理的線程中來執(zhí)行。Worker 的 run() 方法就是直接調(diào)用了 runWorker 這個方法。
final void runWorker(Worker w) {
...
Runnable task = w.firstTask;
w.firstTask = null;
...
while (task != null || (task = getTask()) != null) {
...
task.run();
...
}
...
processWorkerExit(w, completedAbruptly);
}
如果在創(chuàng)建 Worker 時,就指定了一個任務(wù),會先執(zhí)行這個任務(wù)。后面就是循環(huán),不斷地從任務(wù)隊列獲取任務(wù)去執(zhí)行。獲取任務(wù)時,核心線程會一直等待獲取到新的任務(wù)。而一般線程會設(shè)置一個超時時間,這個時間就是創(chuàng)建線程池時指定的 keepAliveTime。超時之后,就退出循環(huán)了,Worker 的使命完成,馬上會被釋放。有兩點要補充一下:
- 核心線程和一般線程沒有區(qū)分,只是去 getTask 時,根據(jù)當(dāng)前線程的數(shù)量是否大于核心線程數(shù)量來決定要不要一直等待。
- 可以設(shè)置 allowCoreThreadTimeOut 為 true,讓核心線程獲取任務(wù)時也會超時。
現(xiàn)在我們基本上搞清楚了線程池是如何調(diào)度線程來執(zhí)行任務(wù)的。再來回顧一下前面 Executors 中創(chuàng)建的幾種線程池。
Executors中創(chuàng)建的CachedThreadPoollExecutor,是用的同步隊列,只有當(dāng)前有線程在等待任務(wù)時,才能加入,實際上也不在隊列中管理,是直接扔給了執(zhí)行線程去執(zhí)行。所以CachedThreadPool中,當(dāng)新任務(wù)到來時,如果線程數(shù)小于核心線程數(shù),是直接創(chuàng)建,不然就看當(dāng)前有沒有在等待任務(wù)的線程,有就交給該線程執(zhí)行,沒有就創(chuàng)建一個新線程去執(zhí)行。
Executors中創(chuàng)建的FixedThreadPoolExecutor和SingleThreadlExecutor,都是核心數(shù)量等于最大數(shù)量,且它們的任務(wù)隊列是無限容量的。當(dāng)新任務(wù)到來時,如果線程數(shù)小于核心線程數(shù),創(chuàng)建新線程去執(zhí)行,不然就加到任務(wù)隊列中等待。
最后,研究一下 ScheduledThreadPoolExecutor 是怎么實現(xiàn)定時任務(wù)的。ScheduledThreadPoolExecutor 實現(xiàn)了 ScheduledExecutorService 接口中的 schedule 等方法。調(diào)用 schedule() 方法時,會把需要定時執(zhí)行的任務(wù)打包在 ScheduledFutureTask 對象中,然后加入到等待執(zhí)行的隊列中去。
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
RunnableScheduledFuture<?> t = decorateTask(command,
new ScheduledFutureTask<Void>(command, null,
triggerTime(delay, unit)));
delayedExecute(t);
return t;
}
ScheduledThreadPoolExecutor 中用 DelayedWorkQueue 來管理等待執(zhí)行的任務(wù)。添加時,會根據(jù)執(zhí)行時間,把任務(wù)排到隊列中合適的位置,保證隊列中的任務(wù)按執(zhí)行時間先后排列。取出時,取隊列頭部的任務(wù),如果隊列頭部沒有任務(wù),或者任務(wù)的執(zhí)行時間還沒到,就要等待。
private void delayedExecute(RunnableScheduledFuture<?> task) {
if (isShutdown())
reject(task);
else {
super.getQueue().add(task);
if (isShutdown() &&
!canRunInCurrentRunState(task.isPeriodic()) &&
remove(task))
task.cancel(false);
else
ensurePrestart();
}
}
delayedExecute() 方法中,先把任務(wù)加入到任務(wù)隊列中,然后調(diào)用 ensurePrestart() 方法去啟動一個新線程(線程數(shù)量小于限定的核心線程數(shù)量才會啟動新線程)。這個線程就會去隊列中等待任務(wù),任務(wù)隊列會在任務(wù)執(zhí)行時間到時返回任務(wù)給線程去執(zhí)行。這樣就實現(xiàn)了定時任務(wù)的執(zhí)行。
4. 從任務(wù)中產(chǎn)生返回值
前面我們用 Runable 來定義任務(wù),但是 Runable 執(zhí)行完成后不會有返回值。當(dāng)需要返回值時,可以實現(xiàn) Callable 接口。Callable 需要通過 ExecutorService 中聲明的 submit() 方法去執(zhí)行。
public interface Callable<V> {
V call() throws Exception;
}
下面舉一個計算年級平均分的例子。為了簡化,假定每個班學(xué)生人數(shù)都是50人。為了計算年級平均分,要讓各班去計算各自的總分。每個班計算總分的過程用 Callable 去執(zhí)行。
private static final int STUDENT_NUM_OF_EACH_CLASS = 50;
static class ClassScoreCaculator implements Callable<Integer> {
private List<Integer> loadScore() {
List<Integer> scoreList = new ArrayList<>();
for (int i = 0; i < STUDENT_NUM_OF_EACH_CLASS; ++i) {
scoreList.add((int) (Math.random() * 100));
}
return scoreList;
}
@Override
public Integer call() throws Exception {
List<Integer> scoreList = loadScore();
Integer sum = 0;
for (Integer score : scoreList) {
sum += score;
}
return sum;
}
}
public static void main(String[] args) {
List<Future<Integer>> results = new ArrayList<>();
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 12; ++i) {
results.add(executor.submit(new ClassScoreCaculator()));
}
int sumScore = 0;
for (Future<Integer> result : results) {
try {
sumScore += result.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
int average = sumScore / (STUDENT_NUM_OF_EACH_CLASS * 12);
System.out.print("average score is " + average);
}
submit() 方法會返回 Future 對象。可以用 get() 方法去獲取執(zhí)行結(jié)果,get() 方法會一直阻塞,直到 Callable 執(zhí)行完成返回結(jié)果。如果不希望阻塞,可以先用 isDone() 方法查詢是否執(zhí)行完成。也使用帶超時時間參數(shù)的 get() 方法。注意到 get() 方法會拋出兩種異常:InterruptedException 和 ExecutionException 。其中,InterruptedException 是調(diào)用 Future 對象的 cancel() 方法去取消任務(wù)時,可能會中斷線程而拋出的異常。而 ExecutionException ,是執(zhí)行任務(wù)過程中的異常。因為,Callable 的 call() 方法是會拋出異常的,這個異常會被封裝到 ExecutionException 中拋出。
5. 休眠
當(dāng)我們需要任務(wù)暫停一段時間,可以使用線程的 sleep() 方法。在線程休眠過程中,可能會有其他線程嘗試中斷當(dāng)前線程,這時 sleep() 方法會拋出 InterruptedException ,結(jié)束休眠。我們可以在 catch 到中斷異常之后,選擇盡快結(jié)束當(dāng)前線程的執(zhí)行任務(wù),當(dāng)然也可以忽略,選擇繼續(xù)執(zhí)行。
public static native void sleep(long millis) throws InterruptedException;
public static void sleep(long millis, int nanos) throws InterruptedException {...}
6. 優(yōu)先級
線程的優(yōu)先級表示線程的重要性,線程調(diào)度器傾向于讓優(yōu)先級高的線程先執(zhí)行。可以用 Thread 的 getPriority() 方法讀取線程的優(yōu)先級,通過 setPriority() 方法可以修改線程的優(yōu)先級。目前 Java 中的線程是映射到底層操作系統(tǒng)的線程,通過底層操作系統(tǒng)來實現(xiàn)的。所以優(yōu)先級也被映射到底層操作系統(tǒng)中的線程優(yōu)先級。但是,不同操作系統(tǒng)的優(yōu)先級級別數(shù)量、策略都有所不同,Java 中的 10 個優(yōu)先級并不能映射得很好。Thinking in Java 書上建議,調(diào)整優(yōu)先級時,只使用 MAX_PRIORITY、NORM_PRIORITY 和 MIN_PRIORITY 三種級別。由于不同操作系統(tǒng)的線程調(diào)度策略不一樣,因此我們在開發(fā)時不應(yīng)該依賴于線程的執(zhí)行順序。
7. 讓步
通過 Thread 的 yield() 方法,可以給線程調(diào)度器一個建議:當(dāng)前線程的工作告一段落,可以讓出 CPU 給其他線程使用了。當(dāng)然,這只是一個建議,沒有任何機制能保證它一定被采納。所以,我們在開發(fā)時也不應(yīng)該依賴于 yield() 方法。
8. 后臺線程
后臺(daemon)線程,也有叫守護(hù)線程的。關(guān)于后臺線程需要了解的主要有三點:
- 當(dāng)所有非后臺線程結(jié)束,程序也就會結(jié)束,所有的后臺進(jìn)程都被殺死。因此,不要把必須執(zhí)行的任務(wù)放到后臺線程中。
- 通過 setDaemon(true) 可以把線程標(biāo)記為后臺線程。這個方法要在線程開始運行之前調(diào)用,不然會拋出異常。
- 后臺線程中創(chuàng)建的線程會被自動設(shè)成后臺線程。原理是線程初始化的時候會獲取當(dāng)前線程的 daemon ,來設(shè)置自己的 daemon 。
下面看一個使用后臺線程的例子。
public class DaemonThreadStudy {
private static class DaemonThreadFactory implements ThreadFactory {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setDaemon(true);
return thread;
}
}
private static class DaemonFromFactory implements Runnable {
@Override
public void run() {
try {
while (true) {
Thread.sleep(100);
System.out.println(Thread.currentThread() + " " + this);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
Executor executor = Executors.newCachedThreadPool(new DaemonThreadFactory());
for (int i = 0; i < 10; i++) {
executor.execute(new DaemonFromFactory());
}
System.out.println("All daemons started");
Thread.sleep(100);
}
}
9. join
一個線程如果要等待另一個線程執(zhí)行完成,可以調(diào)用另一個線程的 join() 方法。調(diào)用 join() 方法之后,當(dāng)前線程將被掛起,等待另一個線程執(zhí)行結(jié)束。join() 方法也有一個帶等待時間參數(shù)的重載版本,等待時間到了后,不管等待的線程是否執(zhí)行完成都會返回。來看一個使用 join() 方法的例子。
public static void main(String[] args) {
Thread sleeper = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " interrupted");
}
System.out.println(Thread.currentThread().getName() + " has awakend");
}
}, "Sleeper");
Thread joiner = new Thread(new Runnable() {
@Override
public void run() {
try {
sleeper.join();
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " interrupted");
}
System.out.println(Thread.currentThread().getName() + " join completed");
}
}, "Joiner");
sleeper.start();
joiner.start();
}
在上面的例子中,sleeper 休眠 10 秒,而 joiner 會一直等待 sleeper 執(zhí)行完成。注意,join() 方法和 sleep() 方法一樣會拋出中斷異常。也就是說,線程在等待時,也可以通過調(diào)用 interrupt 方法去中斷它。
來看一下 join() 方法的實現(xiàn)。
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
實際上就是調(diào)用了線程對象的 wait() 方法。循環(huán)判斷線程是否執(zhí)行結(jié)束,沒結(jié)束就繼續(xù) wait()。如果設(shè)置了超時時間的話,會在時間到了之后結(jié)束 wait(),退出循環(huán)。按照注釋中的說法,Thread 對象在 terminate 的時候,會調(diào)用 notifyAll() 。這樣,wait() 方法就能返回,join() 方法也就執(zhí)行完了。為什么需要設(shè)個循環(huán)去判斷 isAlive() 呢,因為我們有可能在程序的其他地方去調(diào)用被等待的線程對象的 notify() 和 notifyAll() 方法。如果沒有循環(huán)的,join() 就會直接返回,不會等到線程執(zhí)行結(jié)束了。
測試在其他地方調(diào)用被等待的線程的 notify() 方法時,還發(fā)現(xiàn)調(diào)用一個對象的 wait()、notify()、notifyAll() 等方法都需要先成為這個對象的 monitor 所有者,不然會拋出 IllegalMonitorStateException 異常。成為一個對象的 monitor 所有者有三種方法:
- 在這個對象的 synchronize 的方法中
- 在 synchronize 這個對象的代碼塊中
- 如果這個對象是 Class 類的對象,可以在類的靜態(tài)的 synchronize 的方法中
其實三種方法本質(zhì)上都是一樣的,就是在調(diào)用 wait()、notify() 方法之前,得先對對象做 synchronize 。前面兩種就不用說了。第三種方法,由于 Class 類的特殊性,類的靜態(tài)的 synchronize 的方法,實際上就是對 Class 對象做的 synchronize。
10. 線程組
ThreadGroup ,這個東西沒太大作用。看了書和很多資料,都說沒什么意義。看了 ThreadGroup 類的源碼,就是持有一個線程數(shù)組和一個線程組數(shù)組,方便進(jìn)行統(tǒng)一操作,比如:interrupt() 方法。除此之外,還能通過 activeCount() 方法獲取一下線程組內(nèi)的線程數(shù)量。有些作用的是,ThreadGroup 可以對線程運行中沒有被捕獲的異常做處理。
11. 捕獲異常
由于線程的特性,我們無法捕獲從線程中逃逸的異常。一旦異常逃出任務(wù)的 run() 方法,就會向外傳播 。我們需要用特殊的方式捕獲線程中逃出的異常。在 Java 1.5 以前只能用線程組來捕獲,在 1.5 版本之后,就有更好的方式可以處理了。
class ExceptionRunable implements Runnable {
@Override
public void run() {
throw new RuntimeException();
}
}
class NativeExceptionHandling {
public static void main(String[] args) {
try {
Executor executor = Executors.newCachedThreadPool();
executor.execute(new ExceptionRunable());
} catch (Exception e) {
e.printStackTrace();
}
}
}
在上面的示例中,我們執(zhí)行了一個會拋出異常的任務(wù),并嘗試用 try catch 去捕獲異常,很顯然,這是沒有作用的,因為異常是在新的線程中拋出的。那么,我們改怎么去捕獲這種異常呢?Java 1.5 引入了一個新的接口 Thread.UncaughtExceptionHandler,我們可以給線程設(shè)置一個異常處理器,去處理沒有被捕獲的異常。
class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("caught " + e);
}
}
class HandlerThreadFactory implements ThreadFactory {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
return thread;
}
}
class CaptureUncaughtException {
public static void main(String[] args) {
Executor executor = Executors.newCachedThreadPool(new HandlerThreadFactory());
executor.execute(new ExceptionRunable());
}
}
這里我們使用了 HandlerThreadFactory 來創(chuàng)建線程,通過調(diào)用 Thread 的成員方法 setUncaughtExceptionHandler() 給每個線程設(shè)置了 UncaughtExceptionHandler 。線程運行中沒有被捕獲的異常,會被扔給 UncaughtExceptionHandler 來處理,而不會向外傳遞。
進(jìn)一步研究,看異常是怎么被傳到處理器中的。先看 Thread 類中的 dispatchUncaughtException() 方法,這個方法是由 JVM 去調(diào)用的。之前的流程應(yīng)該就是線程執(zhí)行任務(wù)后,有沒捕獲的異常,然后 JVM 調(diào)用線程的 dispatchUncaughtException() 方法來處理異常。然后,獲取異常處理器,把異常交給異常處理器的 uncaughtException() 方法。如果該線程對象設(shè)置了異常處理器,就用自身的,否則就交給線程組處理(ThreadGroup 也實現(xiàn)了 UncaughtExceptionHandler 接口)。
public class Thread {
...
private volatile UncaughtExceptionHandler uncaughtExceptionHandler;
private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;
private ThreadGroup group;
...
/**
* Dispatch an uncaught exception to the handler. This method is
* intended to be called only by the JVM.
*/
private void dispatchUncaughtException(Throwable e) {
getUncaughtExceptionHandler().uncaughtException(this, e);
}
public UncaughtExceptionHandler getUncaughtExceptionHandler() {
return uncaughtExceptionHandler != null ?
uncaughtExceptionHandler : group;
}
...
}
線程組中的處理流程是:首先找父線程組的處理方法;其次找線程中設(shè)置的默認(rèn)異常處理器;都找不到就直接打印異常堆棧。
public class ThreadGroup {
...
public void uncaughtException(Thread t, Throwable e) {
if (parent != null) {
parent.uncaughtException(t, e);
} else {
Thread.UncaughtExceptionHandler ueh =
Thread.getDefaultUncaughtExceptionHandler();
if (ueh != null) {
ueh.uncaughtException(t, e);
} else if (!(e instanceof ThreadDeath)) {
System.err.print("Exception in thread \""
+ t.getName() + "\" ");
e.printStackTrace(System.err);
}
}
}
...
}
總結(jié)一下,線程執(zhí)行中沒捕獲的異常優(yōu)先扔給線程對象中設(shè)置的異常處理器,其次給線程組,如果都沒處理,會看是否設(shè)置了 Thread 類的默認(rèn)異常處理器。
看到這里,我產(chǎn)生了一個疑問,按照這種機制,沒捕獲的異常最多是打個錯誤信息,而不會導(dǎo)致程序 crash 。那么,為什么在 android 中,異常會導(dǎo)致應(yīng)用 crash 呢。原來,Android 在所有進(jìn)程啟動時,都給 Thread 設(shè)置了 defaultUncaughtExceptionHandler ,遇到異常時會讓應(yīng)用 crash 。想了解更多內(nèi)容,請看這篇文章 理解Android Crash處理流程 。
12. 結(jié)語
這篇文章是我閱讀《 Thinking In Java 》書中并發(fā)一章第2節(jié),并結(jié)合源碼以及測試的學(xué)習(xí)記錄。對 Java 基礎(chǔ)線程機制的學(xué)習(xí)到此就告一段落了。下一篇文章學(xué)習(xí)多線程開發(fā)的兩個主要問題的解決:Java多線程開發(fā)(二)| 多線程的競爭與協(xié)作。