Java 并发编程 - 线程基础
1. 线程的创建
首先我们来复习我们学习 java
时接触的线程创建,这也是面试的时候喜欢问的,有人说两种也有人说三种四种等等,其实我们不能去死记硬背,而应该深入理解其中的原理,当我们理解后就会发现所谓的创建线程实质都是一样的,在我们面试的过程中如果我们能从本质出发回答这样的问题,那么相信一定是个加分项!好了我们不多说了,开始今天的 code
之路
1.1 **继承 Thread 类创建线程 **
**
这是我们最常见的创建线程的方式,通过继承
Thread
类来重写run
方法,
代码如下:
测试方法:
结果:
继承
Thread
的线程创建简单,启动时直接调用start
方法,而不是直接调用run
方法。直接调用run
等于调用普通方法,并不是启动线程
1.2 **实现 Runnable 接口创建线程 **
**
上述方式我们是通过继承来实现的,那么在
java
中提供了Runnable
接口,我们可以直接实现该接口,实现其中的run
方法,这种方式可扩展性更高
代码如下:
测试代码:
运行结果:
1.3 实现 Callable 接口创建线程
这种方式是通过 实现
Callable
接口,实现其中的call
方法来实现线程,但是这种线程创建的方式是依赖于 ****FutureTask **
包装器**来创建Thread
, 具体来看代码
代码如下:
测试代码:
结果:
有的时候,我们可能需要让一步执行的线程在执行完成以后,提供一个返回值给到当前的主线程,主线程需要依赖这个值进行后续的逻辑处理,那么这个时候,就需要用到带返回值的线程了
关于线程基础知识的如果有什么问题的可以在网上查找资料学习学习!这里不再阐述
2. 线程的生命周期
Java 线程既然能够创建,那么也势必会被销毁,所以线程是存在生命周期的,那么我们接下来从线程的生命周期开始去了解线程。
2.1 线程的状态
2.1.1 线程六状态认识
线程一共有 6 种状态(NEW、RUNNABLE、BLOCKED、WAITING、TIME_WAITING、TERMINATED)
NEW:初始状态,线程被构建,但是还没有调用 start 方法
RUNNABLED:运行状态,JAVA 线程把操作系统中的就绪和运行两种状态统一称为“运行中”
BLOCKED:阻塞状态,表示线程进入等待状态, 也就是线程因为某种原因放弃了 CPU 使用权,阻塞也分为几种情况
TIME_WAITING:超时等待状态,超时以后自动返回
TERMINATED:终止状态,表示当前线程执行完毕
2.1.2 代码实操演示
代码:
启动一个线程前,最好为这个线程设置线程名称,因为这样在使用 jstack 分析程序或者进行问题排查时,就会给开发人员提供一些提示
2.1.3 线程的状态堆栈
➢ 运行该示例,打开终端或者命令提示符,键入“ jps
”, ( JDK1.5
提供的一个显示当前所有 java
进程 pid
的命令)
➢ 根据上一步骤获得的 pid
,继续输入 jstack pid
(jstack是 java
虚拟机自带的一种堆栈跟踪工具。jstack 用于打印出给定的 java
进程 ID
或 core file
或远程调试服务的 Java
堆栈信息)
3. 线程的深入解析
3.1 线程的启动原理
前面我们通过一些案例演示了线程的启动,也就是调用
start()
方法去启动一个线程,当run
方法中的代码执行完毕以后,线程的生命周期也将终止。调用start
方法的语义是当前线程告诉JVM
,启动调用start
方法的线程。我们开始学习线程时很大的疑惑就是 启动一个线程是使用
start
方法,而不是直接调用run
方法,这里我们首先简单看一下start
方法的定义,在Thread
类中
这里我们能看到
start
方法中调用了native
方法start0
来启动线程,这个方法是在Thread
类中的静态代码块中注册的 , 这里直接调用了一个native
方法registerNatives
由于
registerNatives
方法是本地方法,我们要看其实现源码则必须去下载jdk
源码,关于jdk
及虚拟机hotspot
的源码下载可以去openJDK
官网下载 ,参考:我们可以本地查看源码或者直接去 http://hg.openjdk.java.net/jdk8u/jdk8u60/jdk/file/935758609767/src/share/native/java/lang/Thread.c 查看
Thread
类对应的本地方法.c
文件,
如上图,我们本地下载
jdk
工程,找到src->share->native->java->lang->Thread.c
文件
上面是
Thread.c
中所有代码,我们可以看到调用了RegisterNatives
同时可以看到method
集合中的映射,在调用本地方法start0
时,实际调用了JVM_StartThread
,它自身是由c/c++
实现的,这里需要在 虚拟机源码中去查看,我们使用的都是hostpot
虚拟机,这个可以去openJDK
官网下载,上述介绍了不再多说我们看到
JVM_StartThread
的定义是在jvm.h
源码中,而jvm.h
的实现则在虚拟机hotspot
中,我们打开hotspot
源码,找到src -> share -> vm -> prims ->jvm.cpp
文件,在2955
行,可以直接检索JVM_StartThread
, 方法代码如下:
JVM_ENTRY
是用来定义JVM_StartThread
函数的,在这个函数里面创建了一个真正和平台有关的本地线程, 上述标记 <2> 处
为了进一步线程创建,我们在进入
new JavaThread(&thread_entry, sz)
中查看一下具体实现过程,在thread.cpp
文件1566
行处定义了new
的方法
对于上述代码我们可以看到最终调用了
os::create_thread(this, thr_type, stack_sz);
来实现线程的创建,对于这个方法不同平台有不同的实现,这里不再赘述,
上面都是创建过程,之后再调用
Thread::start(native_thread);
在 JVM_StartThread 中调用,该方法的实现在Thread.cpp
中
start
方法中有一个函数调用:os::start_thread(thread);
,调用平台启动线程的方法,最终会调用Thread.cpp
文件中的JavaThread::run()
方法
3.2 线程的终止
3.2.1 通过标记位来终止线程
正常我们线程内的东西都是循环执行的,那么我们实际需求中肯定也存在想在其他线程来停止当前线程的需要,这是后我们可以通过标记位来实现,所谓的标记为其实就是
volatile
修饰的变量,着由它的可见性特性决定的,如下代码就是依据volatile
来实现标记位停止线程
3.2.2 通过 stop 来终止线程
我们通过查看
Thread
类或者JDK API
可以看到关于线程的停止提供了stop()
,supend()
,resume()
等方法,但是我们可以看到这些方法都被标记了@Deprecated
也就是过时的,虽然这几个方法都可以用来停止一个正在运行的线程,但是这些方法都是不安全的,都已经被抛弃使用,所以在我们开发中我们要避免使用这些方法,关于这些方法为什么被抛弃以及导致的问题
JDK
文档中较为详细的描述 《Why Are Thread.stop, Thread.suspend, Thread.resume and Runtime.runFinalizersOnExit Deprecated?》在其中有这样的描述:
总的来说就是:
3.2.3 通过 interrupt 来终止线程
通过上面阐述,我们知道了使用
stop
方法是不推荐的,那么我们用什么来更好的停止线程,这里就引出了interrupt
方法,我们通过调用interrupt
来中断线程当其他线程通过调用当前线程的
interrupt
方法,表示向当前线程打个招呼,告诉他可以中断线程的执行了,至于什么时候中断,取决于当前线程自己线程通过检查自身是否被中断来进行相应,可以通过
isInterrupted()
来判断是否被中断。
我们来看下面代码:
上述代码中我们可以看到,我们创建了
interrupt-1
线程,其中用interrupt
来判断当前线程是否处于中断状态,如果处于中断状态那么就自然结束线程,这里的结束的具体操作由我们开发者来决定。再创建interrupt-2
线程,代码相对简单不阐述,当执行到某时刻时将线程interrupt-1
设置为中断状态,也就是通知interrupt-1
线程。
线程中断标记复位 :
在上述
interrupt-1
代码中如果加入sleep
方法,那么我们会发现程序报出InterruptedException
错误,同时,线程interrupt-1
也不会停止,这里就是因为中断标记被复位了 ,下面我们来介绍一下关于中断标记复位相关的内容
在线程类中提供了** **
Thread.interrupted
的静态方法,用来对线程中断标识的复位,在上面的代码中,我们可以做一个小改动,对interrupt-1
线程创建的代码修改如下:
上述代码中 我们可以看到,判断当前线程是否处于中断标记为
true
, 如果有其他程序通知则为true
此时进入if
语句中,对其进行复位操作,之后再次判断。执行代码后我们发现interrupt-1
线程不会终止,而会一直执行
Thread.interrupted
进行线程中断标记复位是一种主动的操作行为,其实还有一种被动的复位场景,那就是上面说的当程序出现InterruptedException
异常时,则会将当前线程的中断标记状态复位,在抛出异常前,JVM
会将中断标记isInterrupted
设置为false
在程序中,线程中断复位的存在实际就是当前线程对外界中断通知信号的一种响应,但是具体响应的内容有当前线程决定,线程不会立马停止,具体是否停止等都是由当前线程自己来决定,也就是开发者。
3.3 线程终止 interrupt 的原理
首先我们先来看一下在
Thread
中关于interrupt
的定义:
上面代码中我们可以看到,在
interrupt
方法中最终调用了Native
方法interrupt0
,这里相关在线程启动时说过,不再赘述,我们直接找到hotspot
中jvm.cpp
文件中JVM_Interrupt
方法
JVM_Interrupt
方法比较简单,其中我们可以看到直接调用了Thread.cpp
的interrupt
方法,我们进入其中查看
我们可以看到这里直接调用了
os::interrupt(thread)
这里是调用了平台的方法,对于不同的平台实现是不同的,我们这里如下所示,选择Linux
下的实现os_linux.cpp
中,
在上面代码中我们可以看到,在
1
处拿到OSThread
,之后判断如果interrupt
为false
则在2
处调用OSThread
的set_interrupted
方法进行设置,我们可以进入看一下其实现,发现在osThread.hpp
中定义了一个成员变量volatile jint _interrupted;
而set_interrupted
方法其实就是将_interrupted
设置为true
,之后再通过ParkEvent
的unpark()
方法来唤醒线程。具体的过程在上面进行的简单的注释介绍,
评论