Java高并发学习笔记:Thread详解
Posted 氷泠
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java高并发学习笔记:Thread详解相关的知识,希望对你有一定的参考价值。
1 来源
- 来源:《Java高并发编程详解 多线程与架构设计》,汪文君著
- 章节:第一、二、三章
本文是前三章的笔记整理。
2 概述
本文主要讲述了线程的生命周期、Thread
类的构造方法以及常用API
,最后介绍了线程的关闭方法。
3 线程生命周期
3.1 五个阶段
线程生命周期可以分为五个阶段:
NEW
RUNNABLE
RUNNING
BLOCKED
TERMINATED
3.2 NEW
用new
创建一个Thread
对象时,但是并没有使用start()
启动线程,此时线程处于NEW
状态。准确地说,只是Thread
对象的状态,这就是一个普通的Java
对象。此时可以通过start()
方法进入RUNNABLE
状态。
3.3 RUNNABLE
进入RUNNABLE
状态必须调用start()
方法,这样就在JVM
中创建了一个线程。但是,线程一经创建,并不能马上被执行,线程执行与否需要听令于CPU
调度,也就是说,此时是处于可执行状态,具备执行的资格,但是并没有真正执行起来,而是在等待被调度。
RUNNABLE
状态只能意外终止或进入RUNNING
状态。
3.4 RUNNING
一旦CPU
通过轮询或其他方式从任务可执行队列中选中了线程,此时线程才能被执行,也就是处于RUNNING
状态,在该状态中,可能发生的状态转换如下:
- 进入
TERMINATED
:比如调用已经不推荐的stop()
方法 - 进入
BLOCKED
:比如调用了sleep()
/wait()
方法,或者进行某个阻塞操作(获取锁资源、磁盘IO
等) - 进入
RUNNABLE
:CPU
时间片到,或者线程主动调用yield()
3.5 BLOCKED
也就是阻塞状态,进入阻塞状态的原因很多,常见的如下:
- 磁盘
IO
- 网络操作
- 为了获取锁而进入阻塞操作
处于BLOCKED
状态时,可能发生的状态转换如下:
- 进入
TERMINATED
:比如调用不推荐的stop()
,或者JVM
意外死亡 - 进入
RUNNABLE
:比如休眠结束、被notify()
/nofityAll()
唤醒、获取到某个锁、阻塞过程被interrupt()
打断等
3.6 TERMINATED
TERMINATED
是线程的最终状态,进入该状态后,意味着线程的生命周期结束,比如在下列情况下会进入该状态:
- 线程运行正常结束
- 线程运行出错意外结束
JVM
意外崩溃,导致所有线程都强制结束
4 Thread
构造方法
4.1 构造方法
Thread
的构造方法一共有八个,这里根据命名方式分类,使用默认命名的构造方法如下:
Thread()
Thread(Runnable target)
Thread(ThreadGroup group,Runnable target)
命名线程的构造方法如下:
Thread(String name)
Thread(Runnable target,Strintg name)
Thread(ThreadGroup group,String name)
Thread(ThreadGroup group,Runnable target,String name)
Thread(ThreadGroup group,Runnable target,String name,long stackSize)
但实际上所有的构造方法最终都是调用如下私有构造方法:
private Thread(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals);
在默认命名构造方法中,在源码中可以看到,默认命名其实就是Thread-X
的命令(X为数字):
public Thread() {
this((ThreadGroup)null, (Runnable)null, "Thread-" + nextThreadNum(), 0L);
}
public Thread(Runnable target) {
this((ThreadGroup)null, target, "Thread-" + nextThreadNum(), 0L);
}
private static synchronized int nextThreadNum() {
return threadInitNumber++;
}
而在命名构造方法就是自定义的名字。
另外,如果想修改线程的名字,可以调用setName()
方法,但是需要注意,处于NEW
状态的线程才能修改。
4.2 线程的父子关系
Thread
的所有构造方法都会调用如下方法:
private Thread(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals);
其中的一段源码截取如下:
if (name == null) {
throw new NullPointerException("name cannot be null");
} else {
this.name = name;
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
if (g == null) {
if (security != null) {
g = security.getThreadGroup();
}
if (g == null) {
g = parent.getThreadGroup();
}
}
}
可以看到当前这里有一个局部变量叫parent
,并且赋值为currentThread()
,currentThread()
是一个native
方法。因为一个线程被创建时的最初状态为NEW
,因此currentThread()
代表是创建自身线程的那个线程,也就是说,结论如下:
- 一个线程的创建肯定是由另一个线程完成的
- 被创建线程的父线程是创建它的线程
也就是自己创建的线程,父线程为main
线程,而main
线程由JVM
创建。
另外,Thread
的构造方法中有几个具有ThreadGroup
参数,该参数指定了线程位于哪一个ThreadGroup
,如果一个线程创建的时候没有指定ThreadGroup
,那么将会和父线程同一个ThreadGroup
。main
线程所在的ThreadGroup
称为main
。
4.3 关于stackSize
Thread
构造方法中有一个stackSize
参数,该参数指定了JVM
分配线程栈的地址空间的字节数,对平台依赖性较高,在一些平台上:
- 设置较大的值:可以使得线程内调用递归深度增加,降低
StackOverflowError
出现的概率 - 设置较低的值:可以使得创建的线程数增多,可以推迟
OutOfMemoryError
出现的时间
但是,在一些平台上该参数不会起任何作用。另外,如果设置为0也不会起到任何作用。
5 Thread API
5.1 sleep()
sleep()
有两个重载方法:
sleep(long mills)
sleep(long mills,int nanos)
但是在JDK1.5
后,引入了TimeUnit
,其中对sleep()
方法提供了很好的封装,建议使用TimeUnit.XXXX.sleep()
去代替Thread.sleep()
:
TimeUnit.SECONDS.sleep(1);
TimeUnit.MINUTES.sleep(3);
5.2 yield()
yield()
属于一种启发式方法,提醒CPU
调度器当前线程会自愿放弃资源,如果CPU
资源不紧张,会忽略这种提醒。调用yield()
方法会使当前线程从RUNNING
变为RUNNABLE
状态。
关于yield()
与sleep()
的区别,区别如下:
sleep()
会导致当前线程暂停指定的时间,没有CPU
时间片的消耗yield()
只是对CPU
调度器的一个提示,如果CPU
调度器没有忽略这个提示,会导致线程上下文的切换sleep()
会使线程短暂阻塞,在给定时间内释放CPU
资源- 如果
yield()
生效,yield()
会使得从RUNNING
状态进入RUNNABLE
状态 sleep()
会几乎百分百地完成给定时间的休眠,但是yield()
的提示不一定能担保- 一个线程调用
sleep()
而另一个线程调用interrupt()
会捕获到中断信号,而yield
则不会
5.3 setPriority()
5.3.1 优先级介绍
线程与进程类似,也有自己的优先级,理论上来说,优先级越高的线程会有优先被调度的机会,但实际上并不是如此,设置优先级与yield()
类似,也是一个提醒性质的操作:
- 对于
root
用户,会提醒操作系统想要设置的优先级别,否则会被忽略 - 如果
CPU
比较忙,设置优先级可能会获得更多的CPU
时间片,但是空闲时优先级的高低几乎不会有任何作用
所以,设置优先级只是很大程度上让某个线程尽可能获得比较多的执行机会,也就是让线程自己尽可能被操作系统调度,而不是设置了高优先级就一定优先运行,或者说优先级高的线程比优先级低的线程就一定优先运行。
5.3.2 优先级源码分析
设置优先级直接调用setPriority()
即可,OpenJDK 11
源码如下:
public final void setPriority(int newPriority) {
this.checkAccess();
if (newPriority <= 10 && newPriority >= 1) {
ThreadGroup g;
if ((g = this.getThreadGroup()) != null) {
if (newPriority > g.getMaxPriority()) {
newPriority = g.getMaxPriority();
}
this.setPriority0(this.priority = newPriority);
}
} else {
throw new IllegalArgumentException();
}
}
可以看到优先级处于[1,10]
之间,而且不能设置为大于当前ThreadGroup
的优先级,最后通过native
方法setPriority0
设置优先级。
一般情况下,不会对线程的优先级设置级别,默认情况下,线程的优先级为5,因为main
线程的优先级为5,而且main
为所有线程的父进程,因此默认情况下线程的优先级也是5。
5.4 interrupt()
interrupt()
是一个重要的API
,线程中断的API
有如下三个:
void interrupt()
boolean isInterrupted()
static boolean interrupted()
下面对其逐一进行分析。
5.4.1 interrupt()
一些方法调用会使得当前线程进入阻塞状态,比如:
Object.wait()
Thread.sleep()
Thread.join()
Selector.wakeup()
而调用interrupt()
可以打断阻塞,打断阻塞并不等于线程的生命周期结束,仅仅是打断了当前线程的阻塞状态。一旦在阻塞状态下被打断,就会抛出一个InterruptedException
的异常,这个异常就像一个信号一样通知当前线程被打断了,例子如下:
public static void main(String[] args) throws InterruptedException{
Thread thread = new Thread(()->{
try{
TimeUnit.SECONDS.sleep(10);
}catch (InterruptedException e){
System.out.println("Thread is interrupted.");
}
});
thread.start();
TimeUnit.SECONDS.sleep(1);
thread.interrupt();
}
会输出线程被中断的信息。
5.4.2 isInterrupted()
isInterrupted()
可以判断当前线程是否被中断,仅仅是对interrupt()
标识的一个判断,并不会影响标识发生任何改变(因为调用interrupt()
的时候会设置内部的一个叫interrupt flag
的标识),例子如下:
public static void main(String[] args) throws InterruptedException{
Thread thread = new Thread(()->{
while (true){}
});
thread.start();
TimeUnit.SECONDS.sleep(1);
System.out.println("Thread is interrupted :"+thread.isInterrupted());
thread.interrupt();
System.out.println("Thread is interrupted :"+thread.isInterrupted());
}
输出结果为:
Thread is interrupted :false
Thread is interrupted :true
另一个例子如下:
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread() {
@Override
public void run() {
while (true) {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
System.out.println("In catch block thread is interrupted :" + isInterrupted());
}
}
}
};
thread.start();
TimeUnit.SECONDS.sleep(1);
System.out.println("Thread is interrupted :" + thread.isInterrupted());
thread.interrupt();
TimeUnit.SECONDS.sleep(1);
System.out.println("Thread is interrupted :" + thread.isInterrupted());
}
输出结果:
Thread is interrupted :false
In catch block thread is interrupted :false
Thread is interrupted :false
一开始线程未被中断,结果为false
,调用中断方法后,在循环体内捕获到了异常(信号),此时会Thread
自身会擦除interrupt
标识,将标识复位,因此捕获到异常后输出结果也为false
。
5.4.3 interrupted()
这是一个静态方法,调用该方法会擦除掉线程的interrupt
标识,需要注意的是如果当前线程被打断了:
- 第一次调用
interrupted()
会返回true
,并且立即擦除掉interrupt
标识 - 第二次包括以后的调用永远都会返回
false
,除非在此期间线程又一次被打断
例子如下:
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread() {
@Override
public void run() {
while (true) {
System.out.println(Thread.interrupted());
}
}
};
thread.setDaemon(true);
thread.start();
TimeUnit.MILLISECONDS.sleep(2);
thread.interrupt();
}
输出(截取一部分):
false
false
false
true
false
false
false
可以看到其中带有一个true
,也就是interrupted()
判断到了其被中断,此时会立即擦除中断标识,并且只有该次返回true
,后面都是false
。
关于interrupted()
与isInterrupted()
的区别,可以从源码(OpenJDK 11
)知道:
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
public boolean isInterrupted() {
return this.isInterrupted(false);
}
@HotSpotIntrinsicCandidate
private native boolean isInterrupted(boolean var1);
实际上两者都是调用同一个native
方法,其中的布尔变量表示是否擦除线程的interrupt
标识:
true
表示想要擦除,interrupted()
就是这样做的false
表示不想擦除,isInterrupted()
就是这样做的
5.5 join()
5.5.1 join()
简介
join()
与sleep()
一样,都是属于可以中断的方法,如果其他线程执行了对当前线程的interrupt
操作,也会捕获到中断信号,并且擦除线程的interrupt
标识,join()
提供了三个API
,分别如下:
void join()
void join(long millis,int nanos)
void join(long mills)
5.5.2 例子
一个简单的例子如下:
public class Main {
public static void main(String[] args) throws InterruptedException {
List<Thread> threads = IntStream.range(1,3).mapToObj(Main::create).collect(Collectors.toList());
threads.forEach(Thread::start);
for (Thread thread:threads){
thread.join();
}
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+" # "+i);
shortSleep();
}
}
private static Thread create(int seq){
return new Thread(()->{
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+" # "+i);
shortSleep();
}
},String.valueOf(seq));
}
private static void shortSleep(){
try{
TimeUnit.MILLISECONDS.sleep(2);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
输出截取如下:
2 # 8
1 # 8
2 # 9
1 # 9
main # 0
main # 1
main # 2
main # 3
main # 4
线程1和线程2交替执行,而main
线程会等到线程1和线程2执行完毕后再执行。
6 线程关闭
Thread
中有一个过时的方法stop
,可以用于关闭线程,但是存在的问题是有可能不会释放monitor
的锁,因此不建议使用该方法关闭线程。线程的关闭可以分为三类:
- 正常关闭
- 异常退出
- 假死
6.1 正常关闭
6.1.1 正常结束
线程运行结束后,就会正常退出,这是最普通的一种情况。
6.1.2 捕获信号关闭线程
通过捕获中断信号去关闭线程,例子如下:
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(){
@Override
public void run() {
System.out.println("work...");
while(!isInterrupted()){
}
System.out.println("exit...");
}
};
t.start();
TimeUnit.SECONDS.sleep(5);
System.out.println("System will be shutdown.");
t.interrupt();
}
一直检查interrupt
标识是否设置为true
,设置为true
则跳出循环。另一种方式是使用sleep()
:
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(){
@Override
public void run() {
System.out.println("work...");
while(true){
try{
TimeUnit.MILLISECONDS.sleep(1);
}catch (InterruptedException e){
break;
}
}
System.out.println("exit...");
}
};
t.start();
TimeUnit.SECONDS.sleep(5);
System.out.println("System will be shutdown.");
t.interrupt();
}
6.1.3 volatile
由于interrupt
标识很有可能被擦除,或者不会调用interrupt()
方法,因此另一种方法是使用volatile
修饰一个布尔变量,并不断循环判断:
public class Main {
static class MyTask extends Thread{
private volatile boolean closed = false;
@Override
public void run() {
System.out.println("work...");
while (!closed && !isInterrupted()){
}
System.out.println("exit...");
}
public void close(){
this.closed = true;
this.interrupt();
}
}
public static void main(String[] args) throws InterruptedException {
MyTask t = new MyTask();
t.start();
TimeUnit.SECONDS.sleep(5);
System.out.println("System will be shutdown.");
t.close();
}
}
6.2 异常退出
线程执行单元中是不允许抛出checked
异常的,如果在线程运行过程中需要捕获checked
异常并且判断是否还有运行下去的必要,可以将checked
异常封装为unchecked
异常,比如RuntimeException
,抛出从而结束线程的生命周期。
6.3 假死
所谓假死就是虽然线程存在,但是却没有任何的外在表现,比如:
- 没有日志输出
- 不进行任何的作业
等等,虽然此时线程是存在的,但看起来跟死了一样,事实上是没有死的,出现这种情况,很大可能是因为线程出现了阻塞,或者两个线程争夺资源出现了死锁。
这种情况需要借助一些外部工具去判断,比如VisualVM
、jconsole
等等,找出存在问题的线程以及当前的状态,并判断是哪个方法造成了阻塞。
以上是关于Java高并发学习笔记:Thread详解的主要内容,如果未能解决你的问题,请参考以下文章
尚硅谷JUC高并发编程学习笔记Callable,FutureTask,JUC辅助类
尚硅谷JUC高并发编程学习笔记Callable,FutureTask,JUC辅助类