彻底搞懂Java中的Runnable和Thread
写在前面
今天在阅读ThreadPoolExecutor
源码的时候觉得有些地方理解起来似是而非,很别扭!最后才猛然发现,原来是我自己的问题:没有真正理解Runnable和Thread的含义!
我之前对于Runnable
和Thread
理解的误区在于:“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()
。
可以看到Thread
与Runnable
发生了关联,那么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();
}
}
再次,Thread
与Runnable
是如何发生关联的?通过阅读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中要使用线程异步执行任务,必须结合Thread
和Runnable
来实现。具体来说:使用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()
方法的具体实现中将Runnable
与Thread
关联了起来,具体来说是在通过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
这个缘故导致很多人对于Thread
与Runnable
的认识模棱两可。
再次回顾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的更多相关文章
- 轻松搞懂Java中的自旋锁
前言 在之前的文章<一文彻底搞懂面试中常问的各种“锁”>中介绍了Java中的各种“锁”,可能对于不是很了解这些概念的同学来说会觉得有点绕,所以我决定拆分出来,逐步详细的介绍一下这些锁的来龙 ...
- 一文彻底搞懂Java中的环境变量
一文搞懂Java环境变量 记得刚接触Java,第一件事就是配环境变量,作为一个初学者,只知道环境变量怎样配,在加上各种IDE使我们能方便的开发,而忽略了其本质的东西,只知其然不知其所以然,随着不断的深 ...
- 一文带你搞懂java中的变量的定义是什么意思
前言 在之前的文章中,壹哥给大家讲解了Java的第一个案例HelloWorld,并详细给大家介绍了Java的标识符,而且现在我们也已经知道该使用什么样的工具进行Java开发.那么接下来,壹哥会集中精力 ...
- 一文搞懂--Java中重写equals方法为什么要重写hashcode方法?
Java中重写equals方法为什么要重写hashcode方法? 直接看下面的例子: 首先我们只重写equals()方法 public class Test { public static void ...
- 一文搞懂 Java 中的枚举,写得非常好!
知识点 概念 enum的全称为 enumeration, 是 JDK 1.5 中引入的新特性. 在Java中,被 enum关键字修饰的类型就是枚举类型.形式如下: enum Color { RED, ...
- 来吧,一文彻底搞懂Java中最特殊的存在——null
没事的时候,我并不喜欢逛 P 站,而喜欢逛 programcreek 这些技术型网站,于是那天晚上,在夜深人静的时候,我就发现了一个专注基础但不容忽视的主题.比如说:Java 中的 null 到底是什 ...
- 一篇文章让你搞懂Java中的静态代理和动态代理
什么是代理模式 代理模式是常用的java设计模式,在Java中我们通常会通过new一个对象再调用其对应的方法来访问我们需要的服务.代理模式则是通过创建代理类(proxy)的方式间接地来访问我们需要的服 ...
- 彻底搞懂Java中equals和==的区别
java当中的数据类型和“==”的含义: 1.基本数据类型(也称原始数据类型) :byte,short,char,int,long,float,double,boolean.他们之间的比较,应用双等号 ...
- 来吧,一文彻底搞懂Java中的Comparable和Comparator
大家好,我是沉默王二,今天在逛 programcreek 的时候,我发现了一些专注细节但价值连城的主题.比如说:Java 的 Comparable 和 Comparator 是兄弟俩吗?像这类灵魂拷问 ...
- 彻底搞懂 JS 中 this 机制
彻底搞懂 JS 中 this 机制 摘要:本文属于原创,欢迎转载,转载请保留出处:https://github.com/jasonGeng88/blog 目录 this 是什么 this 的四种绑定规 ...
随机推荐
- ESXi查看底层存储磁盘厂商型号的方式与方法
ESXi查看底层存储磁盘厂商型号的方式与方法 背景 公司一台过保的服务器出现了磁盘告警 Vendor不太靠谱. 过保的机器就不管了 不买他们的服务器也不说一下是啥硬盘. 想自己替换,需要先获取磁盘的型 ...
- element-ui中Select 选择器列表内容居中
<el-select class="my-el-select" v-model="tenantCont" placeholder="请输入机构标 ...
- 【VictoriaMetrics】一个小优化:循环改查表,性能提升56.48 倍
作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢! cnblogs博客 zhihu Github 公众号:一本正经的瞎扯 做了一个 vm-storage 数据文件 merge 的工 ...
- linux root用户密码输入正确还是提示access denied
问题:之前用远程工具连接一直都是好的,第二天上班找开远程工具要输root的密码了,输入用户密码后还是无效,可以确定用户密码是对的,其中有一个远程工具一直是连着的就没有问题. 排查问题: 1.相接用pa ...
- Protocol Buffer命名空间冲突
原文在这里. 什么是Protocol Buffer命名空间冲突? 所有链接到Go二进制文件的Protocol Buffer声明都被插入到一个全局注册表中. 每个Protocol Buffer声明(例如 ...
- FaceFusion:探索无限创意,创造独一无二的面孔融合艺术!
FaceFusion:探索无限创意,创造独一无二的面孔融合艺术! 它使用先进的图像处理技术,允许用户将不同的面部特征融合在一起,创造有趣和令人印象深刻的效果.这个项目的潜在应用包括娱乐.虚拟化妆和艺术 ...
- 5.7 Windows驱动开发:取进程模块函数地址
在笔者上一篇文章<内核取应用层模块基地址>中简单为大家介绍了如何通过遍历PLIST_ENTRY32链表的方式获取到32位应用程序中特定模块的基地址,由于是入门系列所以并没有封装实现太过于通 ...
- 8.1 Windows驱动开发:内核文件读写系列函数
在应用层下的文件操作只需要调用微软应用层下的API函数及C库标准函数即可,而如果在内核中读写文件则应用层的API显然是无法被使用的,内核层需要使用内核专有API,某些应用层下的API只需要增加Zw开头 ...
- 7.3 C/C++ 实现顺序栈
顺序栈是一种基于数组实现的栈结构,它的数据元素存储在一段连续的内存空间中.在顺序栈中,栈顶元素的下标是固定的,而栈底元素的下标则随着入栈和出栈操作的进行而变化.通常,我们把栈底位置设置在数组空间的起始 ...
- C#中DataTable数据导出为HTML格式文件
/// <summary> /// DataTable导出为HTML的Table并保存到本地 /// </summary> /// <param name="d ...