我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情
创建线程
创建线程就只有两种方式,在 Oracle官方文档 中,明确说明了Java实现线程的方式就只有两种:
- 实现 Runnable 接口
- 继承 Thread 类
实现 Runnable 接口
public class RunnableStyle implements Runnable {
public static void main(String[] args) {
// 创建线程
Thread thread = new Thread(new RunnableStyle());
}
@Override
public void run() {
// 线程中要做的事情
System.out.println("用 Runnable 方式实现线程");
}
}
如果只是像上面这个例子这么简单的话还可以使用 Lambda 表达式来创建线程:
public class RunnableStyle {
public static void main(String[] args) {
// 创建线程
Thread thread = new Thread(() -> {
// 线程中要做的事情
System.out.println("用 Runnable 方式实现线程");
});
}
}
注意:使用 Lambda 表达式来创建的线程本质上还是属于实现 Runnable 接口的方式来创建线程的,这并不是一种新的创建线程的方式,只是在这种场景下的一种更方便的写法。
继承 Thread 类
public class ThreadStyle extends Thread {
public static void main(String[] args) {
// 创建线程
Thread thread = new ThreadStyle();
}
@Override
public void run() {
// 线程中要做的事情
System.out.println("用 Thread 方式创建线程");
}
}
比较一下两种实现方式的优劣:
通常情况下我们需要去创建一个线程的时候会选择实现「实现 Runnable 接口」方式,事实上这种方式确实优于「继承 Thread 类」方式。理由有三,如下:
-
程序架构的角度 创建线程的工作和线程要做的工作应该是分开的,也就是解耦的。实现 Runnable 接口的方式就将线程要执行的代码也就是 run() 方法里面的内容以及创建线程的机制也就是 Thread 类进行分离了,从而解耦。而继承 Thread 类的方式就没有做到这一点。
-
执行效率的角度 实现 Runnable 接口的方式可以让我们在后续利用一些线程池之类的工具,减少对资源的损耗。而继承 Thread 方式无法使用这些工具类。
-
代码设计的角度 由于 Java 是不支持多继承的,实现接口的方式肯定是优于继承类的方式。
run() 方法
@Override
public void run() {
// target 为实现 Runnable 接口传入的 Runnable 对象
if (target != null) {
target.run();
}
}
如果在构造 Thread 类时传入了 Runnable 对象则执行 Runnable 对象的 run 方法,如果没有传入 Runnable 对象则什么都不做。
恰恰也是这三行代码为 Thread 类提供了两种创建线程执行单元的方式。两种创建线程的方式在执行具体的新线程中的代码时都是调用的 Thread 类的 run 方法。
实现 Runnable 接口方式也是在 run 方法中调用了在 Runnable 接口中定义的 run 方法,而并不是直接去调用了 Runnable 接口的 run 方法。
继承 Thread 类方式则是通过重写 Thread 类中的 run 方法,在 run 方法中去编写要实现的功能,覆盖掉 Thread 类中的 run 方法,从而使得在调用时执行我们自己编写的新线程的执行单元。
总结
- 实现线程的方式只有两种,分别是「实现 Runnable 接口」和「继承 Thread 类」
- 甚至还能说,创建线程的方式只有一种那就是构造 Thread 类,而 Thread 类中提供了两种实现线程的执行单元的方式
- 方式一:实现 Runnable 接口的 run 方法,并把 Runnable 实例传给 Thread 类
- 方式二:重写 Thread 的 run 方法
思考题:如果同时使用两种方式会怎么样
public static void main(String[] args) {
new Thread(() -> {
System.out.println("实现 Runnable 接口创建的线程");
}) {
@Override
public void run() {
System.out.println("继承 Thread 类创建的线程");
}
}.start();
}
如果已经对 run 方法的源码理解透彻的话,那么这道题是没有任何难度的。
实现 Runnable 接口方式是的执行单元是需要在 Thread 的 run 方法去调用的,但是我们已经重写了 Thread 中的 run 方法,自然无从去调用 Runnable 对象的执行单元了,也就不会被执行。
启动线程
启动线程有且仅有唯一的一种方式:调用 start() 方法。
start() 方法
当我们创建完一个线程调用了 start 方法时,这个线程并不会直接执行,而是等待执行,也就是进入了就绪状态。(调用 start() --> 线程就绪)
实际上调用 start 方法的这个动作仅仅只是去通知了 JVM:我有一个线程已经准备好执行了,你有空的话帮我执行一下。
至于这个线程将要在什么时刻运行,这并不是我们能够决定的,这得由 CPU 的线程调度器去决定。
注意:start 方法的调用过程是「一个父线程去调用一个已经创建的子线程的 start 方法」所以 start 方法的执行实际上是会牵扯到父子两个线程的。
start() 方法源码
public synchronized void start() {
// 1. 检查线程状态
if (threadStatus != 0)
throw new IllegalThreadStateException();
// 2. 添加到线程组
group.add(this);
// 3. 调用 start0
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
start() 方法和 run() 方法的区别:
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println(Thread.currentThread().getName());
});
thread.run();
thread.start();
}
// 执行结果:
// main
// Thread-0
start 方法是开启一个新的线程去执行代码,而 run 方法还是在本线程之中去执行的,并没有开启新的线程。
strat 方法和 run 方法从根本上来说就是不同的。start 方法是线程体系中的方法,而 run 方法并不属于线程体系,只是一个执行单元,这两者有着根本上的区别。
执行单元和线程体系本质上并没有太大的关系。线程体系指的是一个线程的创建、启动、死亡等各种状态,而执行单元指的是我们编写的要让这个线程去执行的代码。在程序的设计中这两者应该是解耦的,这也是我们推荐使用实现 Runnable 接口方式去实现一个线程的原因之一。
停止线程
我们创建启动一个线程并不难,但线程在运行过程中却不是想停就能停的。
通常线程停止的两种情况:
- run 方法中的全部代码运行完毕。
- 线程运行的过程中出现没有捕获的异常。
停止线程概述
如果在运行过程中想要停止线程,我们应该:使用 interrupt 来通知线程停止,而不是强制停止线程
Java 语言中并没有提供可以让线程安全可靠停止的方法,但是 Java 提供了 interrupt 机制来停止线程,并且最好的停止线程的方式也是使用 interrupt。
interrupt 机制是一种合作机制而不是强制机制。将 interrupt 看作一种通知的形式我认为更准确一些:用一个线程去通知另外一个线程停下来。所以说,线程是否停止的最终决定权在线程本身,作为调用方对于其是否停止这件事是无能为力的。
这其实可以类比成我们平时在路上驾驶汽车的过程。当红灯亮起来时,实际上也仅能够做到通知路上的汽车停车,然后司机去响应这个红灯,停止汽车。这里的红灯就是通知,司机就是线程。红灯亮起之后,司机可以决定停车等红灯,司机也可以不要停车决定闯红灯,比如正在执行任务的救护车或消防车。
代码实现
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
int num = 0;
// 是否有通知停止线程,司机看到红灯后的逻辑
while (!Thread.currentThread().isInterrupted()) {
num++;
}
System.out.println("任务执行完毕,num 的值:" + num);
});
thread.start();
Thread.sleep(1000);
thread.interrupt(); // 通知线程停止
}
阻塞状态下停止线程
关于阻塞状态,本文在第四节(线程的生命周期)中会有详细描述。
代码演示
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
int num = 0;
try {
while (num <= 100) {
System.out.println(num++);
Thread.sleep(10);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务执行完毕,num 的值:" + num);
});
thread.start();
Thread.sleep(500);
thread.interrupt(); // 通知线程停止
}
循环条件中并没有判断是否有停止线程,这是因为当线程处于 sleep 状态时会自动的检测是否有停止线程的通知,并以 try-catch
的形式体现出来。
上面的例子有一个有意思的地方:把 try-catch 放到 while 里面去会怎么样?
代码演示:
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
int num = 0;
while (num <= 100) {
System.out.println(num++);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("任务执行完毕,num 的值:" + num);
});
thread.start();
Thread.sleep(500);
thread.interrupt(); // 通知线程停止
}
异常被捕获后线程并没有停止,这并不是因为我们在异常处理中“响应”了通知,而是因为在 sleep 状态中会自动清除停止线程的通知。
这么说吧,你通知我停下来一次,我就停下来一次,在后面的几次 sleep() 中没有被通知停止,当然就不做什么处理了。
小结:在 sleep 状态中线程会自动检测是否有 interrupt
通知,如果有 interrupt
通知,响应的方式是抛出一个 InterruptedException
异常,并清除这个通知。
优雅响应 interrupt
实际开发中,run 方法内部经常需要去调用一些其他的方法。如果是在这些被调用的外部方法中有 sleep 这个动作的话,我们在 run 方法中是并不知道线程在什么情况下会 sleep 的。
所以当我们编写一些需要 sleep 的方法时,一定要注意不能够把 interrupt 通知给自己 消化 掉,应该要通过一些办法将这个 interrupt 通知反映给 run 方法。比如:传递中断和恢复中断
传递中断(推荐)
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
try {
startSleep();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
Thread.sleep(500);
thread.interrupt();
}
// 将 InterruptException 抛出
public static void startSleep() throws InterruptedException {
Thread.sleep(1000);
}
在 startSleep 方法中,让线程进入 sleep 状态,并将 InterruptException 异常抛出在方法声明中传递给上一级的方法去处理。这样一旦 run 方法调用了 startSleep 就必须去处理 InterruptException 异常。而如果在 sleep 过程中有 Interrupt 通知出现,那么 run 方法的编写者就可以在 catch 语句中去处理这个 Interrupt 通知。
恢复中断
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
int num = 0;
// 响应通知
while (!Thread.currentThread().isInterrupted()) {
num++;
startSleep();
}
System.out.println("执行完毕,num = " + num);
});
thread.start();
Thread.sleep(500);
thread.interrupt();
}
public static void startSleep() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
// 处理完自己的中断逻辑后,向上一层调用者传递该中断。
Thread.currentThread().interrupt();
}
}
在某些特定的业务场景中,我们可能必须去处理 InterruptedException 异常。那么就必须要在 catch 块中去再次发出 Interrupt 通知,恢复这个通知。让 run 方法的编写者可以正确的处理 Interrupt 通知。
sleep() 的优雅写法
java.util.concurrent 包下的 TimeUnit 提供了更加优雅的 sleep() 方式:
import java.util.concurrent.TimeUnit;
public class SleepDemo {
public static void main(String[] args) throws InterruptedException {
TimeUnit.HOURS.sleep(1); // 休眠1个小时
TimeUnit.MINUTES.sleep(1); // 休眠1分钟
TimeUnit.SECONDS.sleep(1); // 休眠1秒钟
}
}
以上使用 TimeUnit 类中的 sleep 方法可以让代码看起来更加舒服。直接休眠对应单位的时间,而不是去手动的计算要休眠的毫秒数。
线程生命周期
接下来为您分析 Java 中线程的 6 种状态,以及线程的生命周期。
在这张图片中,包含了线程的所有状态以及每种状态之间的互相转换过程。其中箭头的指向是固定的,单箭头的指向则表明了两个线程的状态是不可逆的,一旦从一端到另一端之后就无法再回去原来的状态。
NEW、RUNNABLE、TERMINATED
-
新创建(NEW) 线程一经创建,也就是去 new 了一个 Thread 类之后,未调用 start 方法之前,这时的线程就是「新创建」的状态
-
可运行(RUNNABLE) 有些地方可能将「可运行」状态称之为「就绪」状态,这两者其实都是「RUNNABLE」状态
调用 start 方法开始,直到 run 方法中的代码执行完毕之前,如果没有其他的操作使得线程状态跑到上图右边的三种状态中去的话,线程将会一直处于「可运行」状态。
注意:线程并没有一个「运行」的状态,就算是正在执行 run() 方法的线程状态也是「可运行」状态。
-
已终止(TERMINATED) 当 run 方法的代码执行完毕或者是抛出未处理的异常的时,线程就会处于「已终止」状态中。这个状态也是线程的最终状态,线程一旦进入了这个状态将无法再回到其他的状态中。
NEW、RUNNABLE、TERMINATED 这三种状态都是单向的,是无法返回的。NEW --> RUNNABLE --> TERMINATED
-
代码演示
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
- 被阻塞(BLOCKED) 如果线程执行了一个被 synchronize 关键字修饰的代码块,并且这个代码块还处于其他线程的执行之中,这时调用的线程就会处于阻塞的状态,等待其他线程执行完毕后再执行。 等待的这个线程在等待的时间内就是处于 BLOCKED 状态。
习惯上来说 BLOCKED、WAITING、TIMED_WAITING 都称之为阻塞状态,而不仅仅是 BLOCKED。
-
等待(WAITING) 和 计时等待(TIMED_WAITING) 当线程阻塞时就会进入等待的状态。其中等待和计时等待非常好理解,有带 time 参数的等待就是计时等待,反之则是等待。
-
代码演示
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
捕获处理这个异常。如果异常是子线程中抛出的,那么主线程对这个异常并不敏感,无法直接捕获处理这个异常。虽然在控制台能看到异常信息,但是这个异常信息是子线程输出的,主线程并无法感知到这个异常,之所以能在控制台看到异常信息,是因为主线程与子线程共用了同一个控制台。
如果还不是很理解上面这段话,你可以跑一下这段代码:
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 这个接口来接收并处理异常。
这个接口的使用方式如下:
/**
* 自定义的线程异常处理器
*/
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
接口,内容如下:
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);
}
}
}