写在前面

今天在阅读ThreadPoolExecutor源码的时候觉得有些地方理解起来似是而非,很别扭!最后才猛然发现,原来是我自己的问题:没有真正理解Runnable和Thread的含义!

我之前对于RunnableThread理解的误区在于:“Runnble和Thread是实现多线程的两种方式,在Java中要实现多线程运行要么实现Runnable接口,要么继承Thread类”。咋一看对于这样的描述似乎也没毛病,但是它没有真正阐述清楚诸如“如何在Java中实现一个线程运行”,“Runnable与Thread的区别是什么”这样的问题。而且我看网上很多中文博客对于类似Runnable与Thread的区别这样的讨论也都大同小异,人云亦云,还是没有真正解答我心中的疑惑。

理解Java中的线程

文本中所说的“线程”都是指操作系统中的线程,如果希望任务能够异步执行,可以通过启动一个线程来实现。

Java是面向对象的编程语言,在Java中通过Thread类来实现对操作系统线程的抽象。

具体来说,在Java中一个操作系统线程与一个Thread对象关联,通过调用Thread对象的start()方法来启动一个操作系统线程执行。

关于Java中Thread类的具体说明详见Thread Objects

至此明确了一个认识:在Java中使用Thread来抽象操作系统中的线程,通过调用Thread对象的start()方法启动一个操作系统线程运行。

在Java中使用线程

上一节已经明确了在Java中通过Thread对象来操作线程,那么具体是怎么实现的呢?

如上图,在调用Thread.start()方法之后,会触发JVM本地方法的调用,随后创建一个新的操作系统线程环境执行Thread.run(),而有意思的是在Thread.run()中最终调用的的Runnable.run(),也就是说:通过Thread.start()启动的线程最终执行的是Runnable.run()

可以看到ThreadRunnable发生了关联,那么Runnable到底是什么呢?它们是如何产生关联的呢?

首先,Runnable是JDK中的一个接口。

public interface Runnable {
public abstract void run();
}

其次,官方Runnble的解释如下:

The Runnable interface should be implemented by any class whose instances are intended to be executed by a thread. The class must define a method of no arguments called run.

直白的翻译:Runnable接口可以被任意打算在线程中执行的类实现,而且实现类必须实现接口中的无参方法run()

换句话说:Runnable是一个任务接口,它的run()方法用于实现在线程中真正的执行逻辑。

更进一步说:如果要在线程中异步执行一些业务操作,可以定义一个实现Runnable接口的类,然后将该Runnable对象传递给Thread对象。

因为Thread的源码实现就是直接调用了Runnable.run(),如下:

@Override
public void run() {
// target是一个Runnable对象
if (target != null) {
target.run();
}
}

再次,ThreadRunnable是如何发生关联的?通过阅读JDK源码可以知道,在Thread中保持了一个Runnable对象的引用。

public class Thread implements Runnable {
/* What will be run. */
private Runnable target;
}

而且,这个Runnable对象只能通过Thread的构造方法注入。

public Thread(Runnable target) {}
public Thread(Runnable target, AccessControlContext acc) {}
public Thread(ThreadGroup group, Runnable target) {}
public Thread(Runnable target, String name) {}
public Thread(ThreadGroup group, Runnable target, String name) {}
public Thread(ThreadGroup group, Runnable target, String name, long stackSize) {}

至此可以得出结论:在Java中要使用线程异步执行任务,必须结合ThreadRunnable来实现。具体来说:使用Runnable定义业务操作,使用Thread启动线程并执行Runnable

为什么说Thread必须要结合Runnable呢?没有Runnable可不可以呢?

Thread的源码实现来看,如果没有Runnable,那么通过Thread仅仅是启动了一个线程,但是在这个线程中什么也不做,这有什么意义呢?

// 实例化Thread对象
Thread thread = new Thread();
// 通过thread对象启动一个线程
// 但是没有给thread对象传递Runnable任务,所以这个线程启动之后并没有做任何有价值的事情就结束了
thread.start(); // Thread.run()
@Override
public void run() {
// 如果没有注入Runnable对象,什么事情也不做直接返回
if (target != null) {
target.run();
}
}

实际上,从JDK官方文档可知,有两种使用Thread对象的基本策略:

1.直接控制Thread对象创建和管理,每次需要开始一个异步任务的时候就实例化一个Thread对象,并调用其start()方法。

// 实例化一个Thread对象表示线程
Thread thread = new Thread();
// 调用Thread对象的start()方法启动线程
thread.start();

2.将线程管理(包括实例化Thread对象和调用start()方法启动操作系统线程)交给Executor框架。

注意:Executor框架提供的接口方法中并没有看到任何Thread对象的身影,而是一个Runnble对象。

public interface Executor {
void execute(Runnable command);
}

实际上,这个要看具体的Executor接口实现类。

ThreadPoolExecutor为例,它在execute()方法的具体实现中将RunnableThread关联了起来,具体来说是在通过ThreadPoolExecutor.Worker进行关联的。ThreadPoolExecutor.Worker是一个内部类,它用于封装一个线程Thread对象和一个任务Runnable对象,从它的构造方法就能非常清晰地看到。

// 如下是JDK中ThreadPoolExecutor.Worker类的定义
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
/** Thread this worker is running in. Null if factory fails. */
final Thread thread; /** Initial task to run. Possibly null. */
Runnable firstTask; Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
}

那么,ThreadPoolExecutor.Worker中的Thread是如何执行通过参数传递的Runnable对象呢?这个要从ThreadPoolExecutor的执行序列图来解答。

如上是调用ThreadPoolExecutor.execute()的执行序列图,在这个过程中最终也是通过Thread调用了传递进来的Rannable对象。

其实ThreadPoolExecutor除了实现Executor.execute()方法,还实现了ExecutorService.submit()方法,在submit()方法中允许执行一个有返回值的Callable任务。关于ThreadPoolExecutor的更多使用细则暂且不再论述,这里主要验证的是在Executor框架中调用线程时最终也是执行Runnable.run()

最后总结

关于Java中线程做如下总结:

1.使用Thread类作为线程的抽象,通过调用Thread.start()启动一个线程。

2.在线程中要执行的业务逻辑通过Runnable定义,具体来说是在Runnable.run()中定义。

一点延申思考

虽然说通过Thread.start()启动的线程最终执行的业务逻辑是在Runnable.run()中定义的,但是它们之间属于协作关系,可以说各自分工明确:Thread对象负责启动线程,Runnable负责定义业务操作。为什么Thread一定实现Runnable接口呢?Thread如果不实现Runnable接口是不是也可以呢?而且很可能正是因为Thread实现了Runnable这个缘故导致很多人对于ThreadRunnable的认识模棱两可。

再次回顾Java中线程的执行流程:Thread.start() -> Thread.start0()执行JVM本地方法 -> Thread.run() -> Runnable.run()

假设Thread类不实现Runnable接口,那么就需要在Thread类中自定义一个run()方法,那这样是否可行呢?坦白讲没什么不可以。

那什么JDK的实现中一定要让Thread类去实现Runnable接口呢?难道仅仅是为了获得一个run()方法吗?带着这个疑惑进行了相关资料的检索,其中一个原因值得参考:为了JVM的向后兼容性

【参考】

Difference between Runnable and Thread in Java

Why does the Thread Class implement Runnable interface

Why does Thread Class implements Runnable Interface [duplicate]

多线程(一) | 聊聊Thread和Runnable

彻底搞懂Java中的Runnable和Thread的更多相关文章

  1. 轻松搞懂Java中的自旋锁

    前言 在之前的文章<一文彻底搞懂面试中常问的各种“锁”>中介绍了Java中的各种“锁”,可能对于不是很了解这些概念的同学来说会觉得有点绕,所以我决定拆分出来,逐步详细的介绍一下这些锁的来龙 ...

  2. 一文彻底搞懂Java中的环境变量

    一文搞懂Java环境变量 记得刚接触Java,第一件事就是配环境变量,作为一个初学者,只知道环境变量怎样配,在加上各种IDE使我们能方便的开发,而忽略了其本质的东西,只知其然不知其所以然,随着不断的深 ...

  3. 一文带你搞懂java中的变量的定义是什么意思

    前言 在之前的文章中,壹哥给大家讲解了Java的第一个案例HelloWorld,并详细给大家介绍了Java的标识符,而且现在我们也已经知道该使用什么样的工具进行Java开发.那么接下来,壹哥会集中精力 ...

  4. 一文搞懂--Java中重写equals方法为什么要重写hashcode方法?

    Java中重写equals方法为什么要重写hashcode方法? 直接看下面的例子: 首先我们只重写equals()方法 public class Test { public static void ...

  5. 一文搞懂 Java 中的枚举,写得非常好!

    知识点 概念 enum的全称为 enumeration, 是 JDK 1.5 中引入的新特性. 在Java中,被 enum关键字修饰的类型就是枚举类型.形式如下: enum Color { RED, ...

  6. 来吧,一文彻底搞懂Java中最特殊的存在——null

    没事的时候,我并不喜欢逛 P 站,而喜欢逛 programcreek 这些技术型网站,于是那天晚上,在夜深人静的时候,我就发现了一个专注基础但不容忽视的主题.比如说:Java 中的 null 到底是什 ...

  7. 一篇文章让你搞懂Java中的静态代理和动态代理

    什么是代理模式 代理模式是常用的java设计模式,在Java中我们通常会通过new一个对象再调用其对应的方法来访问我们需要的服务.代理模式则是通过创建代理类(proxy)的方式间接地来访问我们需要的服 ...

  8. 彻底搞懂Java中equals和==的区别

    java当中的数据类型和“==”的含义: 1.基本数据类型(也称原始数据类型) :byte,short,char,int,long,float,double,boolean.他们之间的比较,应用双等号 ...

  9. 来吧,一文彻底搞懂Java中的Comparable和Comparator

    大家好,我是沉默王二,今天在逛 programcreek 的时候,我发现了一些专注细节但价值连城的主题.比如说:Java 的 Comparable 和 Comparator 是兄弟俩吗?像这类灵魂拷问 ...

  10. 彻底搞懂 JS 中 this 机制

    彻底搞懂 JS 中 this 机制 摘要:本文属于原创,欢迎转载,转载请保留出处:https://github.com/jasonGeng88/blog 目录 this 是什么 this 的四种绑定规 ...

随机推荐

  1. [转帖]TiDB 环境与系统配置检查

    https://docs-archive.pingcap.com/zh/tidb/v6.0/check-before-deployment 本文介绍部署 TiDB 前的环境检查操作,以下各项操作按优先 ...

  2. [转帖]使用s3(minio)为kubernetes提供pv存储

    http://www.lishuai.fun/2021/12/31/k8s-pv-s3/#/%E8%A6%81%E6%B1%82 我们可以通过csi使用s3为kubernetes提供pv存储,当我们申 ...

  3. Python学习之二:不同数据库相同表是否相同的比较方法

    摘要 昨天学习了使用python进行数据库主键异常的查看. 当时想我们有跨数据库的数据同步场景. 对应的我可以对不同数据库的相同表的核心字段进行对比. 这样的话能够极大的提高工作效率. 我之前写过很长 ...

  4. 【图论,网络流】CF1525F Goblins And Gnomes

    Problem Link 你在打怪.你有一个 \(n\) 个点 \(m\) 条边的 DAG,接下来会有 \(k\) 波怪来袭,第 \(i\) 波怪有 \(i\) 个,它们会各自选择走一条路径,要求它们 ...

  5. vue中render函数使用attrs绑定id、class、style、事件(5)

    1.h函数的三个参数 第一个参数是必须的. 类型:{String | Object | Function} 一个 HTML 标签名.一个组件.一个异步组件.或一个函数式组件. 是要渲染的html标签. ...

  6. js设计模式(下)

    引用:(23条消息) <Javascript设计模式与开发实践>关于设计模式典型代码的整理(下):模板方法模式.享元模式.职责链模式.中介者模式.装饰模式.状态模式.适配器模式_QQsil ...

  7. c++全局变量extern

    extern extern 是 C++ 中的一个关键字,用于声明一个变量或函数是在其他文件中定义的.它的作用是告诉编译器在链接时在其他文件中寻找该变量或函数的定义. 在 C++ 中,如果一个变量或函数 ...

  8. go中x/sync/semaphore解读

    semaphore semaphore的作用 如何使用 分析下原理 Acquire TryAcquire Release 总结 参考 semaphore semaphore的作用 信号量是在并发编程中 ...

  9. OCR文字检测与识别系统:融合文字检测、文字识别和方向分类器的综合解决方案

    1. OCR文字检测与识别系统:融合文字检测.文字识别和方向分类器的综合解决方案 前两章主要介绍了DBNet文字检测算法以及CRNN文字识别算法.然而对于我们实际场景中的一张图像,想要单独基于文字检测 ...

  10. 13.4 DirectX内部劫持绘制

    相对于外部绘图技术的不稳定性,内部绘制则显得更加流程与稳定,在Dx9环境中,函数EndScene是在绘制3D场景后,用于完成将最终的图像渲染到屏幕的一系列操作的函数.它会将缓冲区中的图像清空,设置视口 ...