深入理解Java线程状态转移
目录
前言
看到网上关于线程状态转移的博客,好多都没说明白。查了很多资料,汇总一篇,希望通过这一篇,能把这些状态转移解释明白,如果有什么没考虑到的,希望指正
转载注明出处原文地址:https://www.cnblogs.com/darope/p/12748184.html
状态转移图
- 要明白线程转移的详细过程,可以先通过一张图片,了解一个线程的生命周期中,该线程会处在何种状态:
注意:单向箭头表示不可逆
1.0 新建态到就绪态
- 概念:1. 新建态:一个线程被创建出来时候所处的状态 ;2. 就绪态:线程调用start()方法后,便处于可以被操作系统调度的状态,即就绪态。该状态可以由三处转化而来,新建态执行了start、线程阻塞结束、锁池等待队列中的线程获得了锁
Thread t1 = new Thread(
new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("hello : " + i);
}
}
}
);
// t1执行start()之后,处于就绪态,操作系统此时可以分配时间片给该线程,让该线程执行run方法体中的内容
t1.start();
- 该状态对应状态图中的第一步,比较简单,不再赘述
1.1 就绪态到运行态
- 概念:运行态:表示当前线程被操作系统调度,分配了时间片,执行线程中的run方法时的状态。运行态只可以由就绪态的线程转化而来,如果多个线程都处在就绪态,就等待操作系统分配
public static void main(String[] args) {
// 线程1
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("t1 : running");
}
});
t1.start();
// 线程2
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("t2 : running");
}
});
t2.start();
}
- 注:可以看到t1和t2两个线程都运行start()方法后,控制台会随机交叉打印两个线程的输出信息,这种随机,是操作系统随机分配时间片的调度决定的
1.2 运行态到就绪态
1.2.1 时间片用完
- 我们知道,操作系统为了公平,不可能从就绪态里面选择一个,一直执行完,而是随机切换到另外的线程去执行,每个线程分配的执行时间结束,操作系统去调用别的线程,当前刚执行结束的线程便由运行态重新回到就绪态,等待操作系统的再次分配。参考上一个代码例子,t1的线程执行体方法中循环打印100次,t2也是,但是会看到控制台是交叉打印的,说明了这一点
1.2.2 t1.yield() 、Thread.yield();
- 概念:在t1线程体重调用t1.yield(),和Thread.yield();本质上一样,Thread.yield()表示当前线程让渡。线程调用yield()方法,会让该线程重新回到就绪队列,但是yield()让当前线程回到就绪队列后,并不能保证操作系统再次调用不会选择该线程,所以yield()方法不能用来控制线程的执行顺序
public static void main(String[] args) {
// 线程1
Thread t1 = new Thread(() -> {
Thread.yield();
for (int i = 0; i < 10; i++) {
System.out.println("t1 : running " + i);
}
});
t1.start();
// 线程2
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("t2 : running " + i);
}
});
t2.start();
}
- 注意:这个程序我故意把线程让步yield()方法写在线程体刚运行的时候,也就是说,每次操作系统分配给t1线程时间片时候,t1都会让步。但这次的让步不代表t1接下来的方法不会执行,也就是我让步之后,大家再一起抢,t1又抢到了时间片,那么t1本次时间片内便执行接下来的方法,等时间片结束,再次分配t1时间片,t1还会让,再接着抢,抢到和抢不到都有可能。
1.3 运行态到阻塞态
- 概念:阻塞态表示当前线程被由于某种原因,被挂起,也就是被阻塞,正在运行的线程被阻塞后,即使结束阻塞状态也回不去运行态,只能回到就绪态,等待os分配cpu资源去调度
1.3.1 Thread.sleep()
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("hello : " + i);
}
}
);
// t1执行start()之后,处于就绪态,操作系统此时可以分配时间片给该线程
t1.start();
}
- 注意:让当前线程睡眠,该线程被阻塞,睡眠时间结束,该线程接着运行
1.3.2 t2.join()
- 当在t1中调用t2.join()。那么t1会阻塞,一直等待t2执行完毕,才结束阻塞回到就绪态
- 直接看代码:这里我把t1和t2抽出来当做全局静态变量
public class TestThread {
static Thread t1;
static Thread t2;
public static void main(String[] args) {
// 线程1
t1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
if(i == 50) {
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t1 : running " + i);
}
});
t1.start();
// 线程2
t2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
System.out.println("t2 : running " + i);
}
});
t2.start();
}
}
- 解释:这个程序的运行结果是,首选t1,t2挣抢时间片,按系统调度,首先控制台t1和t2都有打印自身的输出信息,当t1执行到i=50的时候,调用了t2.join()。此时控制台会全部打印t2的信息,一直等待t2的循环结束,执行体的run方法结束,再去打印t1剩下的没运行完的循环
- 所以join的流程可以抽象为下面这张图片
1.3.3 t1等待用户输入,等待键盘响应
这个很好理解,比如你就执行一个main函数的主线程,等待输入时,该线程是不会结束的,就是处于阻塞状态。
1.4 阻塞态到就绪态
- 1.3中所有阻塞态结束,比如sleep结束,join后t2执行结束,用户输入了信息回车等。t1会结束阻塞态,但是都是回到就绪态,无法再立即回到运行态
1.5 运行态到等待队列
这里牵扯到对象锁的概念
- 两个线程竞争锁,其中t1释放锁,也就是把所占有的对象锁让出。那么如果不主动唤醒,该线程一直处在等待队列中,得不到操作系统OS的调度
- 概念:等待队列,就是当前线程占有锁之后,主动把锁让出,试自身进入等待队列。此种wait加notify可以保证线程执行的先后顺序。notify()是通知一个等待队列的线程回到锁池队列。notifyAll()是通知所有处在等待队列的线程,都回到锁池队列。
- show me code:
public static void main(String[] args) {
Object o = new Object();
// 线程1
Thread t1 = new Thread(() -> {
synchronized (o) {
for (int i = 0; i < 10; i++) {
try {
if(i == 5) {
// 当i=5的时候,让出对象锁,t1进入等待队列,如果没人通知,t1一直等待,程序不会结束
o.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 : running " + i);
}
}
});
t1.start();
// 线程2
Thread t2 = new Thread(() -> {
synchronized (o) {
for (int i = 0; i < 10; i++) {
System.out.println("t2 : running " + i);
}
// 这里t2得到锁,执行完线程方法之后一定要通知t1停止等待。不然t1结束不了,处在一直等待通知的状态
o.notify();
}
});
t2.start();
}
1.6 运行态到锁池队列
- 参考1.5的程序,在i=5之前,t1占有该对象锁,t2即使start()也得不到运行,原因是该对象锁被t1占有,t2拿不到,所以就进入锁池队列
1.7 等待队列到锁池队列
- 参考1.5的程序,当t1wait之后,让出对象锁,t1进入了等待队列,t2拿到锁,运行完之后,调用notify()让等待队列中的t1进入锁池队列。
1.8 锁池队列到就绪态
- 参考1.5的程序,当t2结束后,通知t1进入锁池队列,t2由于运行结束,处在锁池队列中的t1可以拿到对象锁,进入就绪态,等待操作系统的调度,从而进入运行态
1.9 运行态到死亡态
死亡态不可逆,一旦线程进入死亡态,就再也回不到其他状态
- 死亡态只能由运行态进入,运行态中的线程。例如通过操作系统的不停调度,t1直到把整个run方法中的循环体执行完毕,该线程完成了它的使命,便进入死亡态
版权声明:本文为darope原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。