Java并发编程:Thread与Runnable的底层原理
Posted fntp
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java并发编程:Thread与Runnable的底层原理相关的知识,希望对你有一定的参考价值。
一、Thread类与Runnable接口的渊源
本篇是Java并发编程系列第二章,本章主要讲解Thread类与Runnable接口。
(1)Runnable接口的内容详解
在了解Thread类与Runnable接口之前,我觉得有必要先单独对Runnable接口做一个简略的概述。
首先,对于Runnable来说,Runnable,英文翻译一下,就是可运行的,可执行的。但是我如果给他换一种写法呢?RUNNABLE,眼熟吗?这不就是我上一篇文章中提到的RUNNABLE运行状态吗?
如果我们再看一眼Runnable的接口的源码,我们或许就能知道其中委实了。
Runnable 接口应该由其实例打算由线程执行的任何类实现。该类必须定义一个没有参数的方法,称为run。该接口旨在为希望在活动时执行代码的对象提供通用协议。例如,Runnable 是由类 Thread 实现的。处于活动状态仅意味着线程已启动且尚未停止。此外,Runnable 提供了使类处于活动状态而不是子类化 Thread 的方法。实现 Runnable 的类可以通过实例化 Thread 实例并将其自身作为目标传入而无需子类化 Thread 即可运行。
什么意思?读到这里,有两点问题。
(1)为什么Runnable接口只有一个方法?
(2)既然可以通过集成Thread类来达到实现创建线程类,为什么还要实现Runnable接口?
讲到这里,我觉得需要提一下!在大多数情况下,如果您只打算覆盖 run() 方法而不打算覆盖其他 Thread 方法,则应该直接使用 Runnable 接口。这很重要,除非你打算修改或增强类的基本行为,否则不应继承Thread类。。
下面我们来讲解一下问题的答案。
首先,对于第一个问题。我们只需要看Runnable接口的源码就可以发现端倪。没错,就是这个接口上面的@FunctionalInterface。这是什么?这是函数式接口。只有一个抽象方法,这是函数式接口的定义。这种定义是出现在jdk1.8之前的。其次,我们既然可以直接通过继承Thread类来实现线程的创建,为什么还要去另搞一个Runnable接口呢?继承Thread类就可以达到目的,再去实现Runnable接口,岂不是多此一举?其实不然,通过查阅Thread类源码,我们会得知,Thread类实现了Runnable接口,如下图所示。
这里煎蛋讲一下函数式接口,所谓函数式接口,就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口,并且在形式上,函数式接口可以被隐式转换为 lambda 表达式。
我们现在终于知道了,原来Thread就是Runnable接口的实现类,与Runnable接口不同的是,Thread类中不仅包含重写的run方法,并且包含众多Thread内部的自己的方法。其实读到这里啊,如果你以前阅读过我的上一篇文章,你就能知道,线程的实现,本质上只有一种方式,那就是实现Runnable接口。具体的详细剖析点击此处看我的原文剖析。
我希望你脑海中存在一个疑问:为什么我以继承Thread类的形式来实现线程,却还需要重写Thread的run方法?这是为什么?
我暂且用两张图来说明继承Thread类与实现Runnable接口两种原则上看似不同实则相同的线程实现方式背后的小问题。请看下面两张图:
这两张图说明了什么问题?
- Thread内部存在一个私有化的Runnable类型的成员属性:target。
- Thread作为Runnable的实现类,在底层重写run方法的时候,默认使用target对象来调用run方法。
所以,我来给你解答一下心中的疑惑:java是不支持多继承的,只能单继承,可以多实现接口。正因为你选择了Java中较为鸡肋的继承方式来实现线程,就导致了线程的扩展性被限制死,以至于Thread内部提供了一系列的私有方法来弥补单继承带来的缺点,为以继承方式实现线程提供更多可能的API。这是其一,其二,根据上图源码可知道,如果我继承了Thread类不去重写Run方法,那么就会导致该继承类复用父类Thread的run方法,由于你没通过构造函数传递Runnable的形参,所以target就是空的,那么最终就会走向
exit() 方法,在线程实际退出之前进行清理。
总结:
(1)实现线程本质只有一种方式:实现Runnale接口。因为继承Thread类,在你不使用Thread的其他API的情况下,单纯以实现线程的角度来看,继承Thread来实现线程本质就是在通过Runnable实现线程。
注意,我这里说的是实现线程。要想启动线程,底层依靠的是Thread内部类的native本地方法start0()。关于这个方法底层实现,下文会详解。
(2)Thread类的Start() 方法的底层实现与原理分析
接上文,我们说,实现线程是实现Runnable接口,但是要想启动线程,底层依靠的是Thread内部类的native本地方法start0()。
这个方法是通过c语言写的,这么做的目的,主要是满足:java一次编写,四处运行的理念,这一理念的实现方式,就是JNI机制。何为JNI?(JNI全称就是Java Native Interface,java本地接口),当然了这也离不开JVM的功劳。我画了一个简单的图示意一下底层调用本地方法的原理。如下图所示:
看了我的Java为什么需要虚拟机文章,你就知道了,我们看到的线程是Java的内部线程,真正的线程是说操作系统的线程,当我们调用start0()方法,也就是本地方法的时候,会通过jvm进行编译转录,根据操作系统的不同生成不同的文件,通过动态链接库最后将资源引入,完成对本地方法的调用。
当然了,我也清楚,你不喜欢讲话讲一半,不见真章不死心。我特地寻找了Java的虚拟机源码来简单验证一下。如果你想要下载jdk源码分析,我这里举一个例子,由于离不开对虚拟机源码以及JDK源码的讲解预与演示,我以open-jdk8-hotspot虚拟机作为案例简单分析,下载open-jdk8-hotspot虚拟机源码。
我这里以虚拟机部分作为讲解案例。下载后能看见,hotspot内部的src下有一个os目录,os就是操作系统Operation System。里面做了具体的分类。windows、Linux等。
上面的截图指向的是open-jdk的java虚拟机hotspot虚拟机源码。我们要想溯源,需要根据Thread的本地方法源文件才能溯源,所以我准备了open-jdk8源码,在这里我们找到Thread.c这个文件。如下图:
重点来了,看下图啊:
看到了吗?源码中很清晰的写出了start0()、sleep() 、yield() 等方法,是不是有点欧亨利的感觉?出人意料但又在情理之中。我们知道在java中,start0()方法是线程调用start()方法的最终底层方法,作用是启动一个线程。而sleep() 方法是将线程进入Timed_waiting状态的,这必然需要去更新系统系统状态,所以不用想,sleep就是一个本地方法。再看yield方法,也一样,yield作为一个动词,英文翻译就是放弃,也就是调用此方法的线程主动放弃获得CPU时间片以及其他资源分配,重新进入Ready状态,与其他处于Ready状态的线程进行竞争,竞争到了资源,才会进入Running状态,这里提到的都是第一篇的内容,不明白的读者可以前去看我第一篇文章。
看到这里都已经明白了,接下来就是全局搜索jvm.h文件了。
我们此行目的别忘了,我们是想找到这个start0()
方法调用的背后的底层到底是如何实现的,我们再来看一下Thread.c文件是如何描述start0()这个方法的呢?
我们可以看到,他调用了引入的jvm.h函数集合中的JVM_StartThread方法,那么我们就去jvm.h中查看这个方法:下图是jvm.h的内部函数:
好了,找到了jvm.h接口函数集合文件,也找到了其中的启动线程、睡眠线程、让步线程、中断线程等方法对应的低层实现,接下来就是找到这些接口的具体实现文件,没错就是jvm.cpp文件,于是再次全局搜索jvm.cpp文件:
找到这个文件之后,我们打开这个文件,然后来看一下。
这里发现了以下几点问题:
- 这里仍旧使用了函数,thread函数,他是如何构建CPP线程的?
- 这里有一个重载的JVMWrapper()方法,干嘛用的?(由于这个问题与本文目的关联并不大,所以此问题2不做解读)
接着往下 look look吧。
我们通过阅读源码发现,因为JDK 5 的lang包下的线程中,线程的状态是用来防止重新启动一个线程已经开始,因此我们应该通常要先发现Java线程是null这个问题,然后再做处理。然而JNI附加线程之间有一个小窗口被创建线程对象(Java线程组)和更新它的线程状态,所以我们必须检查。
流程分析:
在底层创建操作系统级别的线程时候,需要进行创建如果说未通过校验,那么就会将非法线程状态标记为true,如果通过了,很好,就会进入正式的线程创建。本地实例化线程是通过使用cpp构造器进行的,通过cpp构造器实例化本地线程。
所以全局搜索Thread.cpp,进入源码分析。我们可以看到,底层创建系统线程的时候进行了OS的调用,让操作系统来创建线程,既然涉及到操作系统,那么一定会有区分。上文中提到得系统分类目录,与此处必定有关联,因此,我们只需要随便以一个操作系统为例,关注create_thread() 函数即可。
os::ThreadType thr_type = os::java_thread;
thr_type = entry_point == &compiler_thread_entry ? os::compiler_thread :
os::java_thread;
// 创建线程的方法 create_thread
os::create_thread(this, thr_type, stack_sz);
随便找一个系统进行查看源码,在hotspot虚拟机源码的src目录下有一个os目录,进去后,我选择了windows目录找到这个cpp文件:
然后直接搜索,create_thread方法,如下图所示,我们就找到了最终底层操作系统创建线程的方法:
本节总结
- Java中的线程与操作系统的线程不是一个线程,他们之间是通过jvm建立联系的,并且是 一 一 对应的。
- Java中的线程启动的方法是通过JNI机制实现的,最终调用create_thread(windows系统中)方法,创建了一个系统线程。
- 他的调用链路是Thread.java->jvm.h->jvm.cpp->thread.cpp->os_windows.cpp->create_thread->osthread。
好了,今天的Java学习笔记总结到这里。总体介绍了Java多线程中的Thread类底层实现原理,与Runnable接口的作用。知其然不知其所以然是很严重的,既然知道Java的内部细节,就要去了解一下内部细节是怎么通过底层代码实现的。一年之前的约定,jvm源码系列详解,也会陆续更新。不过,我想还是有始有终,先把Java并发编程系列更新完毕!本系列可能会非常长,因为大多直接介绍底层实现原理,涉及到源码部分的诸多内容,希望同为钟爱Java、学习Java的兄弟姐妹们,有笔误的地方请一定告知在下,不能误导大家!我会及时更改的!好了,今天的内容到此结束,后续会持续更新,下节预告:
以上是关于Java并发编程:Thread与Runnable的底层原理的主要内容,如果未能解决你的问题,请参考以下文章
Java并发编程:Runnable和Thread实现多线程的区别(含代码)