多线程 | Thread 和 Runnable 执行流程的差异
共 3979字,需浏览 8分钟
·
2021-11-23 19:18
早期文章
上图来自网络
一、查看源码
在 Java 中创建自定义线程通常有两种方法,一种方法是继承 Thread 类,另外一种方法是实现 Runnable 接口。
无论是使用 继承 Thread 类 的方式,还是 实现 Runnable 接口 的方式,本质上是没有差别的。因为 Thread 本身也是实现了 Runnable 接口的。查看 Thread 类的定义,定义如下:
class Thread implements Runnable
如果使用继承 Thread 类的方式,通常,我们会定义一个 MyThread 类来继承 Thread 类,并重写 run 方法,然后 new 一个 MyThread 实例,通过这个实例调用 start 方法,让 JVM 来启动这个线程。
如果使用实现 Runnable 接口的方式,通常,我们也会定义一个 MyRunnable 类来实现 Runnable 接口,并重写 run 方法,然后 new 一个 MyRunnable 实例,但是 MyRunnable 实例是并没有 start 方法的。因此,仍然需要 new 一个 Thread 类的实例,将 MyRunnable 实例当作参数传递给 Thread 的构造方法,最后同样通过 Thread 实例的 start 方法,让 JVM 来启动线程。
这里来看一下,Thread 类的构造方法,Thread 类的构造方法有若干个,现在主要来看它的 无参构造方法 和 传递 Runnable 类型的构造方法,代码如下:
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
两个构造函数,都调用了 init 方法。传递 Runnable 类型参数的构造方法比无参构造方法多了一个 target 传递给 init 方法。来看 init 方法的代码:
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
// ...
this.target = target;
// ...
}
对于 target 参数给出的注释为:
the object whose run() method gets called
表示调用 run() 方法的对象,接着来看一下 Thread 类中的 run 方法,代码如下:
public void run() {
if (target != null) {
target.run();
}
}
代码里会判断 target 是否不为 null,不为 null 则调用 target.run。再来看该方法的注释:
If this thread was constructed using a separate Runnable run object, then that Runnable object's run method is called; otherwise, this method does nothing and returns.
Subclasses of Thread should override this method.
如果此线程是使用单独的 Runnable run 对象构造的,则调用该 Runnable 对象的 run方法;否则,此方法不执行任何操作并返回。接下来通过实例来调试分析一下。
二、实例分析
先来写一个继承 Thread 的类的方式来进行观察,代码如下:
public class MyThread extends Thread {
public void run() {
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
Thread myThread = new MyThread();
myThread.start();
System.out.println(Thread.currentThread().getName());
}
}
此时,分别在 Thread 类的 run 方法下断,在 MyThread 的 run 方法下断,然后调试运行,代码直接中断在了 MyThread 类的 run 方法处。调试运行代码发现,并没有中断在 Thread 类的 run 方法处,按照面向对象的设计来说,子类方法覆盖了父类的方法,当前对象是 子类的实例,则应该直接调用 子类的方法。
再来写一个实现 Runnable 的代码,代码如下:
public class MyRunnable implements Runnable {
public void run() {
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
Runnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
System.out.println(Thread.currentThread().getName());
}
}
同样的操作,在 Thread 类的 run 方法和 MyRunnable 的 run 方法处分别设置断点,然后调试运行,代码首先中断在了 Thread 类的 run 方法,然后继续运行,代码又中断在了 MyRunnable 的 run 方法。此时观察调用栈会看到 MyRunnable 的 run 方法是 Thread 类的 run 方法调用的。
三、总结
通过查看源码以及实例代码调试发现,实现 Runnable 接口 在代码执行的流程上比 继承 Thread 类 的流程要稍微复杂些。不过 实现 Runnable 接口 的方法应该更符合软件的设计原则。当然了,在项目中不建议显式的创建线程,更推荐的是使用线程池。
公众号内回复 【mongo】 下载 SpringBoot 整合操作 MongoDB 的文档。
之前整理的关于 Redis 的文章: