承接上文,本文将介绍:线程的生命周期、如何捕获线程异常、以及可以让并行变串行的join()方法

线程生命周期

线程的6个状态.png

在这张图片中,包含了线程的所有状态以及每种状态之间的互相转换过程。其中箭头的指向是固定的,单箭头的指向则表明了两个线程的状态是不可逆的,一旦从一端到另一端之后就无法再回去原来的状态。

NEW、RUNNABLE、TERMINATED

  1. 新创建(NEW)
    线程一经创建,也就是去 new 了一个 Thread 类之后,未调用 start 方法之前,这时的线程就是「新创建」的状态

  2. 可运行(RUNNABLE)
    有些地方可能将「可运行」状态称之为「就绪」状态,这两者其实都是「RUNNABLE」状态

    调用 start 方法开始,直到 run 方法中的代码执行完毕之前,如果没有其他的操作使得线程状态跑到上图右边的三种状态中去的话,线程将会一直处于「可运行」状态。

    注意:线程并没有一个「运行」的状态,就算是正在执行 run() 方法的线程状态也是「可运行」状态。

  3. 已终止(TERMINATED)
    当 run 方法的代码执行完毕或者是抛出未处理的异常的时,线程就会处于「已终止」状态中。这个状态也是线程的最终状态,线程一旦进入了这个状态将无法再回到其他的状态中。

    NEW、RUNNABLE、TERMINATED 这三种状态都是单向的,是无法返回的。NEW –> RUNNABLE –> TERMINATED

  4. 代码演示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(() -> {
    for (int i = 1; i <= 1000; i++)
    if (i == 500)
    // run() 方法执行中
    System.out.println("thread.state = " + Thread.currentThread().getState()); // RUNNABLE
    });

    System.out.println("thread.state = " + thread.getState()); // NEW

    thread.start();
    System.out.println("thread.state = " + thread.getState()); // RUNNABLE

    thread.join();
    System.out.println("thread.state = " + thread.getState()); // TERMINATED
    }

BLOCKED、WAITING、TIMED_WAITING

  1. 被阻塞(BLOCKED)
    如果线程执行了一个被 synchronize 关键字修饰的代码块,并且这个代码块还处于其他线程的执行之中,这时调用的线程就会处于阻塞的状态,等待其他线程执行完毕后再执行。
    等待的这个线程在等待的时间内就是处于 BLOCKED 状态。

    习惯上来说 BLOCKED、WAITING、TIMED_WAITING 都称之为阻塞状态,而不仅仅是 BLOCKED。

  2. 等待(WAITING)计时等待(TIMED_WAITING)
    当线程阻塞时就会进入等待的状态。其中等待和计时等待非常好理解,有带 time 参数的等待就是计时等待,反之则是等待。

  3. 代码演示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    public class Task implements Runnable {
    public static void main(String[] args) throws InterruptedException {
    Task task = new Task();

    Thread t1 = new Thread(task);
    Thread t2 = new Thread(task);
    t1.start();
    t2.start();
    Thread.sleep(5); // 让 t1 和 t2 先跑一会

    System.out.println("t1 sleep(50) t1.state = "+t1.getState()); // TIMED_WAITING
    System.out.println("t1 get LOCK,t2 be BLOCKED t2.state = " + t2.getState()); // BLOCKED

    Thread.sleep(100); // 进入 wait() 中
    System.out.println("t1 wait(), t1.state = " + t1.getState()); // WAITING
    }

    @Override
    public void run() {
    synchronized(this) {
    try {
    Thread.sleep(50);
    wait();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }
    }

捕获线程异常

由于 Runnable 和 Thread 之间的解耦设计,导致 Thread 在运行 Runnable 时其实是无法感知到 Runnable 内部的运行逻辑的,Thread 无法知道这个线程执行完了是否需要处理返回值,是否会抛出什么异常等等

例如线程池,线程池中的 Thread 根本不知道会传进来一个什么样的 Runnable,对于 Thread 而言,只关注执行上的逻辑,而不关注 Runnable 内部的逻辑。

毫无疑问这样的设计是优秀的,但如果线程在执行的过程当中发生了异常,那处理起来就不是很方便了。针对这种情况可以利用 Thread.UncaughtExceptionHandler 捕获异常。

如果主线程抛出一个异常,可以直接使用 try-catch 捕获处理这个异常。如果异常是子线程中抛出的,那么主线程对这个异常并不敏感,无法直接捕获处理这个异常。虽然在控制台能看到异常信息,但是这个异常信息是子线程输出的,主线程并无法感知到这个异常,之所以能在控制台看到异常信息,是因为主线程与子线程共用了同一个控制台。

如果还不是很理解上面这段话,你可以跑一下这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
new Thread(() -> {
throw new RuntimeException("我是子线程抛出来的异常");
}).start();
for (int i = 0; i < 10000; i++) {
System.out.println(i);
if (i == 5000) {
try {
throw new RuntimeException("我是主线程抛出来的异常");
} catch (RuntimeException e) {
System.out.println(e.getMessage());
}
}
}
}

对于主线程的异常,可以直接使用 try-catch 来捕获处理。对于子线程的异常,每次抛出异常的时机都不相同,主线程无法知道什么时候子线程会抛出异常,更无法捕获处理子线程中的异常。

其实这个例子也很形象的向我们展示了线程的工作过程:每个线程在执行期间是互不打扰的,自己干自己的事情。每个线程在领取到自己的任务(run方法)之后,等待被分配资源开始执行(start方法)之后便是一直闷头干下去,直到把工作做完(run方法执行完毕)

这样就要求子线程必须能够自行处理异常,也就是在子线程内部使用 try-catch 来处理异常。但子线程不一定能够处理自己的异常,有些异常需要向外通知父线程,让父线程去执行对应的处理逻辑。

这样看来,子线程更像是父线程的一个方法,异常可以在方法中使用 try-catch 自行处理,也可以标记在方法签名上向外抛出。

针对需要通知父线程处理的异常,可以使用 Thread.UncaughtExceptionHandler 这个接口来接收并处理异常。

这个接口的使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* 自定义的线程异常处理器
*/
private static final class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {

private final String name;

public MyUncaughtExceptionHandler(String name) {
this.name = name;
}

@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println(name + ":成功捕获了异常。抛出异常的线程是:" + t.getName() + "。抛出的异常是:" + e);
}
}

public static void main(String[] args) {
// 方式一:设置为默认异常处理器
// MyUncaughtExceptionHandler --> Thread.defaultUncaughtExceptionHandler
Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler("默认的线程异常处理器"));
new Thread(new Task()).start();

// 方式二:设置为线程专用的异常处理器
// MyUncaughtExceptionHandler --> t.uncaughtExceptionHandler
Thread t = new Thread(new Task(), "Thread-0");
t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler("为 Thread-0 专门设置的异常处理器"));
t.start();
}

private static final class Task implements Runnable {
@Override
public void run() {
throw new RuntimeException("Task 抛出的异常");
}
}

首先创建一个类实现 Thread.UncaughtExceptionHandler 作为自定义的线程异常处理器,再根据需要设置为默认的异常处理器,或者是为每个线程单独设置的异常处理器。

如果没有单独设置异常处理器也没有设置默认的异常处理器,那么调用 ThreadGroup 类对异常做处理,该类实现了 Thread.UncaughtExceptionHandler 接口,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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);
}
}
}

并行变串行,join()

join方法的作用是同步。在主线程中去创建并启动一个线程,再调用这个线程的join方法之后,会使得两个线程原本是并行关系变成串行关系,也就是主线程将会等待子线程执行完毕之后再继续执行。

注意:join方法可以传入一个long类型的参数,表示过了多少毫秒之后两个线程将由串行关系再次转变成并行关系。但如果传入的参数是0的话,表示的是永久等待,也就是主线程将会等待直到子线程执行完毕之后再次执行,相当于不传参数的join方法。

代码演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Test implements Runnable {

@Override
public void run() {
System.out.println("子线程" + Thread.currentThread().getName() + "开始执行");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("子线程" + Thread.currentThread().getName() + "执行完毕");
}

public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new Test());
Thread thread2 = new Thread(new Test());
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("所有子线程执行完毕");
}
}

// main方法执行结果
// 子线程Thread-0开始执行
// 子线程Thread-1开始执行
// 子线程Thread-0执行完毕
// 子线程Thread-1执行完毕
// 所有子线程执行完毕

上面的代码如果将两个线程执行join方法的那行代码注释掉,则执行结果为

1
2
3
4
5
所有子线程执行完毕
子线程Thread-0开始执行
子线程Thread-1开始执行
子线程Thread-1执行完毕
子线程Thread-0执行完毕

很明显,join方法的调用会使得主线程去等待子线程执行完毕之后再重新执行代码。

join期间被中断

如果主线程调用子线程的 join() 方法后,在子线程执行的期间,有 interrupt 通知进入了,怎么办?

针对上面的问题,我再重申一下关于 join() 方法作用的介绍。主线程将会等待调用了join方法的子线程执行完毕后再继续执行

实际上,是主线程在等待子线程执行完毕,也就是说陷入阻塞状态的是主线程而不是子线程。所以关于上面的问题如果有 interrupt 通知进入了主线程将会抛出一个 InterruptedException 来响应这个 interrupt 通知。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static Thread mainThread = Thread.currentThread();

public static void main(String[] args) {
Thread thread = new Thread(() -> {
mainThread.interrupt();
});
thread.start();

try {
thread.join();
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "线程被中断了");
}
}

// main方法执行结果
main线程被中断了

启动一个子线程并调用join方法,这时主线程就在等待子线程的执行完毕,然后子线程去中断了主线程。也就是中断了一个正在因join方法陷入阻塞的线程,那么此时我们中断的是这个陷入阻塞的线程,而不是正在执行的子线程。

join期间的线程状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static Thread mainThread = Thread.currentThread();

public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println(mainThread.getState());
System.out.println(Thread.currentThread().getState());
});
thread.start();
thread.join();
}

// main方法执行结果
// WAITING
// RUNNABLE

在子线程中去打印主线程和子线程各自的状态,明显调用了join方法的主线程被阻塞了是WAITING状态,而正在运行的子线程则是RUNNABLE状态。

join() 方法分析

join() 方法源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public final void join() throws InterruptedException {
join(0);
}

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;
}
}
}

由此可知,join() 方法实际上还是调用了 wait() 方法的。如果没有传入时间参数,则是调用了 wait(0) 这个方法,代表永久等待,直到被唤醒。
有意思的是这其中并没有看到 notify() 或者是 notifyAll() 方法,也就是并没有线程去唤醒这个等待子线程执行完毕的主线程,但是当子线程执行完毕之后,这确确实实被唤醒了。

我们知道,主线程被唤醒的条件是子线程执行完毕,又知道线程执行完毕只有两种情况,一是 run() 方法运行结束,二是抛出了运行时异常。至此,答案水落石出,当线程执行完毕时,将会去执行 notifyAll() 方法唤醒其他的线程。

注意:我们并不提倡使用 Thread 类的实例作为 synchronized 的锁对象原因也是在此,因为这可能会破坏原有的 wait-notify 结构。