Java多線程開發(fā)(一)| 基本的線程機制

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 的使命完成,馬上會被釋放。有兩點要補充一下:

  1. 核心線程和一般線程沒有區(qū)分,只是去 getTask 時,根據(jù)當(dāng)前線程的數(shù)量是否大于核心線程數(shù)量來決定要不要一直等待。
  2. 可以設(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é)作

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,197評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,415評論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,104評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,884評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,647評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,130評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,208評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,366評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,887評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,737評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,478評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,174評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,586評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,827評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,608評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,914評論 2 372

推薦閱讀更多精彩內(nèi)容