线程核心一:实现多线程的正确姿势
实现多线程到底有几种
网上有说 2 种,3 种,4 种,6 种等等 🤦♂️
我们看 Oracle 官网 API 是怎么描述的。
官方描述为两种
:
-
继承 Thread 类
-
实现 Runnable 接口
有两种方法可以创建新的执行线程。 一种是将一个类声明为 Thread 的子类。 该子类应重写 Thread 类的 run 方法。 然后可以分配并启动子类的实例。
public class ThreadTest extends Thread {
@Override
public void run() {
System.out.println("线程执行....");
}
public static void main(String[] args) {
new ThreadTest().start();
}
}
创建线程的另一种方法是声明一个实现 Runnable 接口的类。 然后,该类实现 run 方法。 然后可以分配该类的实例,在创建 Thread 时将其作为参数传递并启动。
public class RunnableTest implements Runnable{
@Override
public void run() {
System.out.println("线程执行....");
}
public static void main(String[] args) {
new Thread(new RunnableTest()).start();
}
}
两种方式的对比
实现 Runnable 接口相对于继承 Thread 类来说,有如下显著的好处:
-
1、适合多个相同程序代码的线程去处理同一资源的情况,把虚拟 CPU(线程)同程序的代码,数据有效的分离,较好地体现了面向对象的设计思想。
-
2、可以避免由于 Java 的单继承特性带来的局限。我们经常碰到这样一种情况,即当我们要将已经继承了某一个类的子类放入多线程中,由于一个类不能同时有两个父类,所以不能用继承 Thread 类的方式,那么,这个类就只能采用实现 Runnable 接口的方式了。
-
3、有利于程序的健壮性,代码能够被多个线程共享,代码与数据是独立的。当多个线程的执行代码来自同一个类的实例时,即称它们共享相同的代码。多个线程操作相同的数据,与它们的代码无关。当线程被构造时,需要的代码和数据通过一个对象作为构造函数实参传递进去,这个对象就是一个实现了 Runnable 接口的类的实例。
两种方法的本质
区别:
通过 Thread 的 run()方法源码我们可以看到如下代码:
@Override
public void run() {
if (target != null) {
target.run();
}
}
如果 target 不等于 null 则,调用 target 的 run 方法,因此我们可以猜到 target 就是 Runnable 对象。
/* What will be run. */
private Runnable target;
由于我们通过继承 Thread 类的时候已经重写了 run()方法,所以并不会执行这段代码。
如果我们是通过实现 Runnable 接口的话,在创建 Thread 对象的时候就通过构造器传入了当前实现 Runnable 接口的对象,所以 target 不等于 null。
由此我们可以知道:
-
继承 Thread 类:run()方法整个被重写
-
实现 Runnable 接口:最终调用 target.run()
思考:如果同时继承了 Thread 类又实现了 Runnable 会出现什么情况?
public static void main(String[] args) {
new Thread(() -> {
System.out.println("我来自Runnable");
}) {
@Override
public void run() {
System.out.println("我来自Thread");
}
}.start();
}
我来自Thread
简单点一句话来说,就是我们覆盖了 Thread 类的 run(),里面的 target 那几行代码都被我们覆盖了。所以肯定不会执行 Runnable 的 run()方法了。
总结
准确的讲,创建线程只有一种方式那就是构造 Thread 类,而实现线程的执行单元有两种方式。
-
实现 Runnable 接口的 run()方法,并把 Runnable 实例传给 Thread 类。
-
继承 Thread 类,重写 Thread 的 run()方法。
典型错误观点分析
线程池
创建线程也算是一种新建线程的方式
我们通过线程池源码,可以看到底层还是通过 Thread 类来新建一个线程传入了我们的 Runnable。
通过 Callable 和 FutureTask 创建线程,也算是一种新建线程的方式
就不过过多赘述了,很清楚的可以看到,还是使用到了 Thread 类,和实现 Runnable 接口。
无返回值是实现 Runnable 接口,有返回值是实现 callable 接口,所以 callable 是新的实现线程的方式
还是通过 Runnable 接口来实现的。
典型错误观点总结
多线程的实现方式,在代码中写法千变万化,但其本质万变不离其宗。
线程核心二:多线程启动的正确姿势
start()方法和 run()方法区别是什么?
代码演示:
public static void main(String[] args) {
Runnable runnable = () -> {
System.out.println(Thread.currentThread().getName());
};
runnable.run();
new Thread(runnable).start();
}
main
Thread-0
我们可以发现执行了 run()方法是有主线程来执行的,并不是新建了一个线程。
run()和 start()的区别可以用一句话概括:单独调用 run()方法,是同步执行;通过 start()调用 run(),是异步执行。
start()方法原理解读
- start()方法含义:
启动新线程
-
start()方法调用后,并不意味着该线程立马运行,只是通知 JVM 在一个合适的时间运行该线程。 有可能很长时间都不会运行,比如遇到饥饿的情况。
-
调用 start()的先后顺序并不能决定线程执行的顺序。
准备工作
首先会让自己处于就绪状态。就绪状态指的是,我已经获取到除了 CPU 以外的其他资源。比如该线程已经设置了上下文,栈,线程状态,以及 PC, 做完准备工作后,线程才可以被 JVM 或者操作系统进一步调度到执行状态。调度到执行状态后,等待获取 CPU 资源,然后才会进入到运行状态,执行 run()方法里面的代码。
不能重复调用start()方法
不然会出现异常:java.lang.IllegalThreadStateException
start()方法源码解析
- 启动新线程检查线程状态
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
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 */
}
}
}
我们看到第一行语句就是:
if (threadStatus != 0)
throw new IllegalThreadStateException();
而 threadStatus 初始化就是 0。
/* Java thread status for tools,
* initialized to indicate thread 'not yet started'
*/
private volatile int threadStatus = 0;
-
加入线程组
-
调用本地方法 start0()
run()方法原理解读
在 Thread 类源码中我们之前已经看过了只有三行代码,其实只是一个普普通通的方法。
@Override
public void run() {
if (target != null) {
target.run();
}
}
线程核心三:线程停止、中断的正确姿势
如何正确停止线程?
使用 interrupt()方法 来通知
,而不是强制
。
interrupt() 字面上是中断的意思,但在 Java 里 Thread.interrupt()方法实际上通过某种方式通知线程,并不会直接中止该线程。
因为相对于开发人员,被停止线程的本身更清楚什么时候停止。
与其说如何正确停止线程,不如说是如何正确通知线程。
代码示例:
public class ThreadTest implements Runnable {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new ThreadTest());
thread.start();
Thread.sleep(500);
thread.interrupt();
}
@Override
public void run() {
int i = 0;
while (i <= Integer.MAX_VALUE / 2) {
if (i % 20000 == 0) {
System.out.println(i);
}
i++;
}
System.out.println("任务执行完成");
}
}
由于打印出来数据特别的多,我就只展示最后一部分输出结果:
1073680000
1073700000
1073720000
1073740000
任务执行完成
感兴趣的话,可以试一下该段代码,可以发现在 0.5 秒后发起的通知线程中断并没有反应,我们的 run() 方法还是执行到了最后。(执行时间超过 0.5 秒)
这样我们也证实了 interrupt () 方法的确是没有立即暂停线程。
我们需要在 while 条件里增加一个判断,在每一次循环时候判断是否已经发起通知中断请求。
public class ThreadTest implements Runnable {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new ThreadTest());
thread.start();
Thread.sleep(500);
thread.interrupt();
}
@Override
public void run() {
int i = 0;
while (!Thread.currentThread().isInterrupted() && i <= Integer.MAX_VALUE / 2) {
if (i % 20000 == 0) {
System.out.println(i);
}
i++;
}
System.out.println("任务执行完成");
}
}
运行结果:
70900000
70920000
70940000
70960000
任务执行完成
我们可以很清楚的看到,1073740000
70960000
两个数值差距非常大,证明的确是在 0.5 秒后就中断了线程。
另外一种情况就是在线程睡眠的时候我们通知中断会怎样?
上代码:
public class ThreadTest {
public static void main(String[] args) {
Runnable runnable = () -> {
try {
int i = 0;
while (!Thread.currentThread().isInterrupted() && i <= 300) {
if (i % 100 == 0) {
System.out.println(i);
}
i++;
}
Thread.sleep(5000);
} catch (InterruptedException e) {
System.out.println("线程在睡眠中被吵醒了!");
e.printStackTrace();
}
};
Thread thread = new Thread(runnable);
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
}
}
运行结果:
0
100
200
300
线程在睡眠中被吵醒了!
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.suanfa.thread.ThreadTest.lambda$main$0(ThreadTest.java:16)
at java.lang.Thread.run(Thread.java:748)
通过代码我们可以看到在睡眠中我们进行通知中断的话会报出InterruptedException
异常,所以在写代码的过程中要及时处理 InterruptedException 才能正确停止线程。
另还有一种情况就是在循环中每次线程都会睡眠的时候我们通知中断会怎样?
上代码:
public class ThreadTest {
public static void main(String[] args) {
Runnable runnable = () -> {
try {
int i = 0;
while (!Thread.currentThread().isInterrupted() && i <= 30000) {
if (i % 100 == 0) {
System.out.println(i);
}
i++;
Thread.sleep(10);
}
} catch (InterruptedException e) {
System.out.println("线程在睡眠中被吵醒了!");
e.printStackTrace();
}
};
Thread thread = new Thread(runnable);
thread.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
}
}
运行结果:
0
100
200
300
400
线程在睡眠中被吵醒了!
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.suanfa.thread.ThreadTest.lambda$main$0(ThreadTest.java:14)
at java.lang.Thread.run(Thread.java:748)
结果还是会在 5 秒后抛出了InterruptedException
异常。
但是我们其实不用在 while 条件中加入!Thread.currentThread().isInterrupted()
的判断,因为在通知中断时候,发现线程在 sleep 中的话,也会进行中断。
如果循环中包含sleep或者wait等方法则不需要在每次循环中检查是否已经收到中断请求。
实际开发中的两种最佳实践
- 第一种:优先选择:
传递
中断
我们先看一段代码:
public class ThreadTest {
public static void main(String[] args) {
Runnable runnable = () -> {
while(!Thread.currentThread().isInterrupted()){
System.out.println("执行了while里的代码");
throwInMethod();
}
};
Thread thread = new Thread(runnable);
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
}
private static void throwInMethod() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
这段代码看起来貌似没什么问题。但是请注意一定一定不要在最内层来进行try/catch
。否则就会如下结果所示:
执行了while里的代码
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.suanfa.thread.ThreadTest.throwInMethod(ThreadTest.java:24)
at com.suanfa.thread.ThreadTest.lambda$main$0(ThreadTest.java:9)
at java.lang.Thread.run(Thread.java:748)
执行了while里的代码
执行了while里的代码
执行了while里的代码
执行了while里的代码
执行了while里的代码
我们发现异常是抛出来了,但是外面的 run()方法依旧在进行 while 循环。并且由于已经抛出了InterruptedException
异常,我们的 while 条件中的!Thread.currentThread().isInterrupted()
已经被重置了。所以会一直循环下去,稍不注意线程就无法被回收。
解决办法:将异常抛给 run() 方法来解决。
public class ThreadTest {
public static void main(String[] args) {
Runnable runnable = () -> {
try {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("执行了while里的代码");
throwInMethod();
}