深入学习Java的线程的生命周期
线程的状态/生命周期
五种状态
这是从 操作系统 层面来描述的
- 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
- 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
- 【运行状态】指获取了 CPU 时间片运行中的状态
当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换 - 【阻塞状态】
如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入【阻塞状态】
等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们 - 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
六种状态
这是从 Java API 层面来描述的。
根据 Thread.State 枚举,Java中线程的状态分为6种:
1、初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
2、运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
3、阻塞(BLOCKED):表示线程阻塞于锁。
4、等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
5、超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
6、终止(TERMINATED):表示该线程已经执行完毕。
状态之间的变迁如下图所示:
掌握这些状态可以让我们在进行Java程序调优时可以提供很大的帮助。
线程常见方法
还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁。
sleep与 yield
sleep方法
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞),不会释放对象锁
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出InterruptedException
- 睡眠结束后的线程未必会立刻得到执行
- 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
- sleep当传入参数为0时,和yield相同
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("执行完成");
}
},"t1");
t1.start();
log.debug("线程t1的状态:"+t1.getState());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("线程t1的状态:"+t1.getState());
//t1.interrupt();
在没有利用 cpu 来计算时,不要让 while(true) 空转浪费 cpu,这时可以使用 yield 或 sleep 来让出 cpu 的使用权给其他程序。
while(true) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 可以用 wait 或 条件变量达到类似的效果
- 不同的是,后两种都需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景
- sleep 适用于无需锁同步的场景
yield方法
- yield会释放CPU资源,让当前线程从 Running 进入 Runnable状态,让优先级更高(至少是相同)的线程获得执行机会,不会释放对象锁;
- 假设当前进程只有main线程,当调用yield之后,main线程会继续运行,因为没有比它优先级更高的线程;
- 具体的实现依赖于操作系统的任务调度器
- 比如ConcurrentHashMap#initTable 方法中就使用了yield方法,
这是因为ConcurrentHashMap中可能被多个线程同时初始化table,但是其实这个时候只允许一个线程进行初始化操作,其他的线程就需要被阻塞或等待,但是初始化操作其实很快,这里Doug Lea大师为了避免阻塞或者等待这些操作引发的上下文切换等等开销,就让其他不执行初始化操作的线程干脆执行yield()方法,以让出CPU执行权,让执行初始化操作的线程可以更快的执行完成。
线程的优先级
线程优先级会提示调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它。如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用。
在Java线程中,通过一个整型成员变量priority来控制优先级,优先级的范围从1~10,在线程构建的时候可以通过setPriority(int)方法来修改优先级,默认优先级是5,优先级高的线程分配时间片的数量要多于优先级低的线程。
设置线程优先级时,针对频繁阻塞(休眠或者I/O操作)的线程需要设置较高优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。在不同的JVM以及操作系统上,线程规划会存在差异,有些操作系统甚至会忽略对线程优先级的设定。
Runnable task1 = () -> {
int count = 0;
for (;;) {
System.out.println("t1---->" + count++);
}
};
Runnable task2 = () -> {
int count = 0;
for (;;) {
// Thread.yield();
System.out.println("t2---->" + count++);
}
};
Thread t1 = new Thread(task1, "t1");
Thread t2 = new Thread(task2, "t2");
// t1.setPriority(Thread.MIN_PRIORITY);
// t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
join方法
等待调用join方法的线程结束之后,程序再继续执行,一般用于等待异步线程执行完结果之后才能继续运行的场景。
为什么需要 join?
下面的代码执行,打印 count 是什么?
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
log.debug("开始执行");
Thread t1 = new Thread(() -> {
log.debug("开始执行");
SleepTools.second(1);
count = 5;
log.debug("执行完成");
},"t1");
t1.start();
log.debug("结果为:{}", count);
log.debug("执行完成");
}
输出
19:30:09.614 [main] DEBUG com.tuling.learnjuc.base.ThreadJoinDemo - 开始执行
19:30:09.660 [t1] DEBUG com.tuling.learnjuc.base.ThreadJoinDemo - 开始执行
19:30:09.660 [main] DEBUG com.tuling.learnjuc.base.ThreadJoinDemo - 结果为:0
19:30:09.662 [main] DEBUG com.tuling.learnjuc.base.ThreadJoinDemo - 执行完成
19:30:10.673 [t1] DEBUG com.tuling.learnjuc.base.ThreadJoinDemo - 执行完成
分析
- 因为主线程和线程 t1 是并行执行的,t1 线程需要 1 秒之后才能算出 count=5
- 而主线程一开始就要打印 count的结果,所以只能打印出 count=0
解决方法
- 用 sleep 行不行?为什么?
- 用 join,加在 t1.start() 之后即可
实现同步
以调用方角度来讲,如果:
- 需要等待结果返回,才能继续运行就是同步
- 不需要等待结果返回,就能继续运行就是异步
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
log.debug("开始执行");
Thread t1 = new Thread(() -> {
log.debug("开始执行");
SleepTools.second(1);
count = 5;
log.debug("执行完成");
},"t1");
t1.start();
//SleepTools.second(1);
t1.join();
log.debug("结果为:{}", count);
log.debug("执行完成");
}
输出
19:36:05.192 [main] DEBUG com.tuling.learnjuc.base.ThreadJoinDemo - 开始执行
19:36:05.235 [t1] DEBUG com.tuling.learnjuc.base.ThreadJoinDemo - 开始执行
19:36:06.239 [t1] DEBUG com.tuling.learnjuc.base.ThreadJoinDemo - 执行完成
19:36:06.239 [main] DEBUG com.tuling.learnjuc.base.ThreadJoinDemo - 结果为:5
19:36:06.240 [main] DEBUG com.tuling.learnjuc.base.ThreadJoinDemo - 执行完成
面试题
现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?
可以利用join()方法实现,把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行。比如在线程T2中调用了线程T1的join()方法,直到线程T1执行完毕后,才会继续执行线程T2剩下的代码。
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
log.debug("线程t1执行完成");
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
t1.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("线程t2执行完成");
}
},"t2");
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
try {
t2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("线程t3执行完成");
}
},"t3");
t1.start();
t2.start();
t3.start();
守护线程
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
log.debug("开始运行...");
Thread t1 = new Thread(() -> {
log.debug("开始运行...");
SleepTools.second(3);
log.debug("运行结束...");
}, "t1");
// 设置t1线程为守护线程
t1.setDaemon(true);
t1.start();
SleepTools.second(1);
log.debug("运行结束...");
输出
21:21:58.104 [main] DEBUG com.tuling.learnjuc.base.DaemonThreadDemo - 开始运行...
21:21:58.143 [t1] DEBUG com.tuling.learnjuc.base.DaemonThreadDemo - 开始运行...
21:21:59.158 [main] DEBUG com.tuling.learnjuc.base.DaemonThreadDemo - 运行结束...
守护线程的应用场景
守护线程看起来好像没什么用?但是实际上它的作用很大。
- 比如在JVM中垃圾回收器就采用了守护线程,如果一个程序中没有任何用户线程,那么就不会产生垃圾,垃圾回收器也就不需要工作了。
- 在一些中间件的心跳检测、事件监听等涉及定时异步执行的场景中也可以使用守护线程,因为这些都是在后台不断执行的任务,当进程退出时,这些任务也不需要存在,而守护线程可以自动结束自己的生命周期。
从这些实际场景中可以看出,对于一些后台任务,当不希望阻止JVM进程结束时,可以采用守护线程。
线程的终止
线程自然终止
要么是run执行完成了,要么是抛出了一个未处理的异常导致线程提前结束。
面试题: 如何正确终止正在运行的线程?
stop(不要使用)
stop()方法已经被jdk废弃,调用stop方法无论run()中的逻辑是否执行完,都会释放CPU资源,释放锁资源。这会导致线程不安全,因为该方法会导致两个问题:
- 立即抛出ThreadDeath异常,在run()方法中任何一个执行指令都可能抛出ThreadDeath异常。
- 会释放当前线程所持有的所有的锁,这种锁的释放是不可控的。
比如:线程A的逻辑是转账(获得锁,1号账户减少100元,2号账户增加100元,释放锁),那线程A刚执行到1号账户减少100元就被调用了stop方法,释放了锁资源,释放了CPU资源。1号账户平白无故少了100元。
public class ThreadStopDemo {
private static final Object lock = new Object();
private static int account1 = 1000;
private static int account2 = 0;
public static void main(String[] args) {
Thread threadA = new Thread(new TransferTask(),"threadA");
threadA.start();
// 等待线程A开始执行
SleepTools.ms(50);
// 假设在转账过程中,我们强制停止了线程A
threadA.stop();
//验证锁是否释放
// synchronized (lock){
// System.out.println("主线程加锁成功");
// }
}
static class TransferTask implements Runnable {
@Override
public void run() {
synchronized (lock) {
try{
System.out.println("开始转账...");
// 1号账户减少100元
account1 -= 100;
// 休眠100ms
SleepTools.ms(50);
// 假设在这里线程被stop了,那么2号账户将不会增加,且锁会被异常释放
System.out.println("1号账户余额: " + account1);
account2 += 100; // 2号账户增加100元
System.out.println("2号账户余额: " + account2);
System.out.println("转账结束...");
}catch (Throwable t){
System.out.println("线程A结束执行");
t.printStackTrace();
}
}
}
}
}
因此,在实际应用中,一定不能使用stop()方法来终止线程,那么如何安全地实现线程的终止呢?
中断机制
安全的中止则是其他线程通过调用某个线程A的interrupt()方法对其进行中断操作, 中断好比其他线程对该线程打了个招呼,“A,你要中断了”,不代表线程A会立即停止自己的工作,同样的A线程完全可以不理会这种中断请求。线程通过检查自身的中断标志位是否被置为true来进行响应,
线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()来进行判断当前线程是否被中断,不过Thread.interrupted()会同时将中断标识位改写为false。
如果一个线程处于了阻塞状态(如线程调用了thread.sleep、thread.join、obj.wait等),则在线程在检查中断标示时如果发现中断标示为true,则会在这些阻塞方法调用处抛出InterruptedException异常,并且在抛出异常后会立即将线程的中断标示位清除,即重新设置为false。
不建议自定义一个取消标志位来中止线程的运行。因为run方法里有阻塞调用时会无法很快检测到取消标志,线程必须从阻塞调用返回后,才会检查这个取消标志。这种情况下,使用中断会更好,因为,
1、一般的阻塞方法,如sleep等本身就支持中断的检查,
2、检查中断位的状态和检查取消标志位没什么区别,用中断位的状态还可以避免声明取消标志位,减少资源的消耗。
注意:处于死锁状态的线程无法被中断
中断正常运行的线程
中断正常运行的线程, 不会清空中断状态
Thread t1 = new Thread(()->{
while(true) {
Thread current = Thread.currentThread();
boolean interrupted = current.isInterrupted();
if(interrupted) {
log.debug(" 中断状态: {}", interrupted);
break;
}
}
}, "t1");
t1.start();
//中断线程t1
t1.interrupt();
log.debug("中断状态:{}",t1.isInterrupted());
输出
20:45:17.596 [t1] DEBUG com.tuling.learnjuc.base.ThreadInterruptDemo - 中断状态: true
20:45:17.596 [main] DEBUG com.tuling.learnjuc.base.ThreadInterruptDemo - 中断状态:true
中断 sleep,wait,join 的线程
这几个方法都会让线w程进入阻塞状态,中断线程会清空中断状态。以 sleep 为例:
Thread t1 = new Thread(()->{
while(true) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1");
t1.start();
Thread.sleep(100);
//中断线程t1
t1.interrupt();
log.debug("中断状态:{}",t1.isInterrupted());