文章目录
(一)状态转换
(1)操作系统中的线程状态转换
在现在的操作系统中,线程是被视为轻量级进程的,所以操作系统线程的状态其实和操作系统进程的状态是一致的。
操作系统线程主要有以下三个状态:
【1】就绪状态(ready):线程正在等待使用CPU,经调度程序调用之后可进入running状态。
【2】执行状态(running):线程正在使用CPU。
【3】等待状态(waiting): 线程经过等待事件的调用或者正在等待其他资源(如I/O)
(2)Java线程状态
Java线程状态。 线程可以处于以下状态之一:
NEW
尚未启动的线程处于此状态。
RUNNABLE
在Java虚拟机中执行的线程处于此状态。
BLOCKED
被阻塞等待监视器锁定的线程处于此状态。
WAITING
正在等待另一个线程执行特定动作的线程处于此状态。
TIMED_WAITING
正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。
TERMINATED
已退出的线程处于此状态。
在操作系统层面,Java 线程中的 BLOCKED、WAITING、TIMED_WAITING 是一种状态,即前面我们提到的休眠状态。也就是说只要 Java 线程处于这三种状态之一,那么这个线程就永远没有 CPU 的使用权
如果想要确定线程当前的状态,可以通过 getState() 方法,并且线程在任何时刻只可能处于 1 种状态。
public State getState() {
// get current thread state
return sun.misc.VM.toThreadState(threadStatus);
}
public static State toThreadState(int var0) {
if ((var0 & 4) != 0) {
return State.RUNNABLE;
} else if ((var0 & 1024) != 0) {
return State.BLOCKED;
} else if ((var0 & 16) != 0) {
return State.WAITING;
} else if ((var0 & 32) != 0) {
return State.TIMED_WAITING;
} else if ((var0 & 2) != 0) {
return State.TERMINATED;
} else {
return (var0 & 1) == 0 ? State.NEW : State.RUNNABLE;
}
}
Thread thread = new Thread(() -> {});
System.out.println(thread.getState());
thread.start();
System.out.println(thread.getState());
(二)Java六个状态分析
(1) New 新创建
New 表示线程被创建但尚未启动的状态:当我们用 new Thread() 新建一个线程时,如果线程没有开始运行 start() 方法,所以也没有开始执行 run() 方法里面的代码,那么此时它的状态就是 New。而一旦线程调用了 start(),它的状态就会从 New 变成 Runnable
(2)Runnable 可运行
Java 中的 Runable 状态对应操作系统线程状态中的两种状态,分别是 Running 和 Ready,也就是说,Java 中处于 Runnable 状态的线程有可能正在执行,也有可能没有正在执行,正在等待被分配 CPU 资源。
所以,如果一个正在运行的线程是 Runnable 状态,当它运行到任务的一半时,执行该线程的 CPU 被调度去做其他事情,导致该线程暂时不运行,它的状态依然不变,还是 Runnable,因为它有可能随时被调度回来继续执行任务。
(3)Blocked 被阻塞
从 Runnable 状态进入 Blocked 状态只有一种可能,就是进入 synchronized 保护的代码时没有抢到 monitor 锁,无论是进入 synchronized 代码块,还是 synchronized 方法,都是一样。当处于 Blocked 的线程抢到 monitor 锁,就会从 Blocked 状态回到Runnable 状态。
(4)Waiting 等待
线程进入 Waiting 状态有三种可能性。
没有设置 Timeout 参数的 Object.wait() 方法。
没有设置 Timeout 参数的 Thread.join() 方法。
LockSupport.park() 方法。
Blocked 与 Waiting 的区别是 Blocked 在等待其他线程释放 monitor 锁,而 Waiting 则是在等待某个条件,比如 join 的线程执行完毕,或者是 notify()/notifyAll() 。
(5)Timed Waiting 等待
在 Waiting 上面是 Timed Waiting 状态,这两个状态是非常相似的,区别仅在于有没有时间限制,Timed Waiting 会等待超时,由系统自动唤醒,或者在超时前被唤醒信号唤醒。
以下情况会让线程进入 Timed Waiting 状态。
【1】设置了时间参数的 Thread.sleep(long millis) 方法;
【2】设置了时间参数的 Object.wait(long timeout) 方法;
【3】设置了时间参数的 Thread.join(long millis) 方法;
【4】设置了时间参数的 LockSupport.parkNanos(long nanos) 方法和 LockSupport.parkUntil(long deadline) 方法。
(6)Terminated 终止
Terminated 终止状态,要想进入这个状态有两种可能。
【1】run() 方法执行完毕,线程正常退出。
【2】出现一个没有捕获的异常,终止了 run() 方法,最终导致意外终止。
线程执行完 run() 方法后,会自动转换到 TERMINATED 状态,当然如果执行 run() 方法的时候异常抛出,也会导致线程终止。有时候我们需要强制中断 run() 方法的执行,我们可以直接调用 Thread 类里面的 stop() 方法,不过它已经标记为 @Deprecated,所以不建议使用了。正确的姿势其实是调用 interrupt() 方法。
(三)实践案例分析
(1)反复调用同一个线程的start()方法是否可行?
【1】反复调用同一个线程的start()方法是否可行?
【2】假如一个线程执行完毕(此时处于TERMINATED状态),再次调用这个线程的start()方法是否可行?
查看Thread类中start()方法源码,代码如下
public synchronized void start() {
//threadStatus表示处于NEW状态的线程
if (threadStatus != 0)
throw new IllegalThreadStateException();
//通知当前线程的线程组这个线程将要启动,并添加当前线程到线程组中
//当前线程组未启动线程数减少
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
//处理启动失败的线程
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
//本地方法执行线程的实际启动流程
private native void start0();
在start()内部,这里有一个threadStatus的变量。如果它不等于0,调用start()是会直接抛出异常的。
我是在start()方法内部的最开始打的断点,叙述下在我这里打断点看到的结果:
测试代码如下
@Test
public void testThreadState(){
Thread thread = new Thread(()->{
System.out.println("Thread Run...");
});
thread.start();
thread.start();
}
第一个 thread.start();执行情况如下
第二个 thread.start();执行情况如下
两个问题的答案都是不可行,在调用一次start()之后,threadStatus的值会改变(threadStatus !=0),此时再次调用start()方法会抛出IllegalThreadStateException异常。
比如,threadStatus为2代表当前线程状态为TERMINATED。
(2)分析如下代码检查问题
当前线程被中断之后,是否会正常退出while(true),线程是否正常结束?其中try-catch包含在while(true)内部?
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (true){
if (Thread.currentThread().isInterrupted()){
break;
}
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
//主线程休眠500ms,让测试线程运行一个时间段
Thread.sleep(500);
thread.interrupt();
}
上述代码运行后,查看控制台运行情况可以发现子线程响应了中断信号,但是程序依然正常运行,出现上述问题的原因主要如下
执行thread.interrupt()后会不会结束线程的运行,这个线程的中断标记位就会被设置成 true。在上面代码中捕获了InterruptedException,但是并没有向外抛出异常或者再次响应中断通知。
我们知道对于响应中断通知,有俩个正确的处理方式,一是方法签名抛异常,而是在catch中再次中断。
上述代码中,在抛出InterruptedException后会清除中断标志(代表可以接收下一个中断信号了)。所以循环体再次执行Thread.currentThread().isInterrupted()还是返回的是false
上面的代码可以在catch处再次中断,抛出一个InterruptedException异常,try catch捕捉此异常,应该重置一下中断标示,因为抛出异常后,中断标示会自动清除掉!
(3)Waiting 状态和Blocked 状态的分析
想要从 Blocked 状态进入 Runnable 状态,要求线程获取 monitor 锁,而从 Waiting 状态流转到其他状态则比较特殊,因为首先 Waiting 是不限时的,也就是说无论过了多长时间它都不会主动恢复。
处于Waiting状态的线程,如果其他线程调用 notify() 或 notifyAll()来唤醒它,它会直接进入 Blocked 状态,这是为什么呢?因为唤醒 Waiting 线程的线程如果调用 notify() 或 notifyAll(),要求必须首先持有该 monitor 锁,所以处于 Waiting 状态的线程被唤醒时拿不到该锁,就会进入 Blocked 状态,直到执行了 notify()/notifyAll() 的唤醒它的线程执行完毕并释放 monitor 锁,才可能轮到它去抢夺这把锁,如果它能抢到,就会从 Blocked 状态回到 Runnable 状态。
关于是否拥有锁的方面,BLOCKED说明当前线程为获取到锁,WAITING是当前线程已获得锁执行了等待操作所处于的状态,执行等待和唤醒的线程都是基于已经获取锁后的操作。
调用wait()方法前线程必须持有对象的锁。线程调用wait()方法时,会释放当前的锁,直到有其他线程调用notify()/notifyAll()方法唤醒等待锁的线程。
需要注意的是,其他线程调用notify()方法只会唤醒单个等待锁的线程,如有有多个线程都在等待这个锁的话不一定会唤醒到之前调用wait()方法的线程。
同样,调用notifyAll()方法唤醒所有等待锁的线程之后,也不一定会马上把时间片分给刚才放弃锁的那个线程,具体要看系统的调度。
处于Waiting状态的线程会释放因为之前已经获取了锁,在运行状态切换至Waiting状态时会释放掉锁资源。
Waiting状态如果被唤醒后会处于就绪状态,如果该线程需要重新获取锁,那么在竞争锁的时候如果未获得锁则也会进入BLOCKED状态。
(4)wait(),notify()实践以及唤醒后线程执行位置分析
notify方法仅仅唤醒一个线程,令线程开始执行。所以,如果有多个线程等待一个对象的时候,这个方法只能唤醒一个线程。而唤醒的线程的选择是依赖于操作系统对于线程的管理的。
notifyAll方法会唤醒所有等待改对象的线程,尽管哪一个线程会优先执行时取决于操作系统的线程调度的。
如图:当一个拥有Object锁的线程调用 wait()方法时,就会使当前线程加入object.wait 等待队列中,并且释放当前占用的Object锁,这样其他线程就有机会获取这个Object锁,获得Object锁的线程调用notify()方法,就能在Object.wait 等待队列中随机唤醒一个线程(该唤醒是随机的与加入的顺序无关,优先级高的被唤醒概率会高),若果调用notifyAll()方法就唤醒全部的线程。注意:调用notify()方法后并不会立即释放object锁,会等待该线程执行完毕后释放Object锁。
【问题思考】
Java中进入wait状态的线程被唤醒后会接着上次执行的地方往下执行还是会重新执行临界区的代码?
public class Main {
private synchronized void count(){
for (int i = 0; i < 10; i++) {
if(i == 5){
try {
System.out.println("进入 wait 状态...");
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(i);
}
}
public static void main(String[] args) throws InterruptedException {
Main main = new Main();
new Thread(main::count).start();
Thread.sleep(2000);
synchronized (main){
System.out.println("唤醒线程...");
main.notifyAll();
}
}
}
上述代码中,线程在执行一定循环次数后进入wait状态,之后会被重新唤醒,观察控制台输出可以看到被唤醒的线程虽然需要重新获取锁,但是在获取锁之后就会继续上次执行位置继续执行。
(5)java.lang.IllegalMonitorStateException异常
public class IllegalMonitorStateException
extends RuntimeException
抛出以表示线程已尝试在对象的监视器上等待或通知其他线程等待对象的监视器,而不拥有指定的监视器。
public class Example {
public static void main(String[] args) throws InterruptedException {
Example example = new Example();
example.wait();
example.notify();
}
}
出现该问题的原因是因为:
我们在使用wait和notify等方法时没有在临界资源内使用,Java中每个类和对象都有一个同步锁,只有获取到调用同步锁,调用wait和notify方法才行。
API中所说的监视器(monitor)可以理解成同步锁。想要执行某个对象的notify(), notifyAll(),wait(), wait(long), wait(long, int)方法就必须获取该对象的锁,需要使用synchronized,不然就会抛出IllegalMonitorStateException异常。
(四)wait notify 实现生产者模式
生产者和消费者用一个中间媒介通信,即数据容器。其中当容器队列未满则生产者持续生产数据,如果队列已满则生产者休息,通知消费者消费。如果队列空,则消费者阻塞休息,通知生产者生产
生产者代码
public class Producer implements Runnable {
private Container container;
public Producer(Container container) {
this.container = container;
}
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println("生产数据===="+i);
container.put(i);
}
}
}
消费者从容器中取出数据
public class Consumer implements Runnable {
private Container container;
public Consumer(Container container) {
this.container = container;
}
@Override
public void run() {
while (true){
System.out.println("消费数据===="+container.take());
}
}
}
import java.util.LinkedList;
/**
* 自定义容器
*/
public class Container {
/** 容器允许存放的最大数量 **/
private final int maxSize;
/** 容器 **/
private final LinkedList<Integer> container;
public Container(int maxSize, LinkedList<Integer> container) {
this.maxSize = maxSize;
this.container = container;
}
/**
* 往队列添加元素,如果队列已满则阻塞线程
*/
public synchronized void put(Integer data){
//如果队列已满,则阻塞生产者线程
if (container.size()==maxSize){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//队列未满则添加元素,并通知消费者消费数据
container.add(data);
notify();
}
/**
* 从队列取出数据,如果队列为空则阻塞
* @return 队列元素
*/
public synchronized Integer take(){
//如果队列为空,则消费者停止消费
if (container.size()==0){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//队列不为空则消费数据,并通知生产者继续生产数据
Integer remove = container.poll();
notify();
return remove;
}
}
注意代码中 notify 的位置,并不是等到队列满或空了才唤醒对方,而是每生产或消费一次都会唤醒对方,即生产者与消费者交替唤醒。当然,这并不意味着交替运行,真正的运行时机由线程获取时间片决定。
(五)总结
(1)总结
【1】线程的状态是需要按照箭头方向来走的,比如线程从 New 状态是不可以直接进入 Blocked 状态的,它需要先经历 Runnable 状态。
【2】线程生命周期不可逆:一旦进入 Runnable 状态就不能回到 New 状态;一旦被终止就不可能再有任何状态的变化。所以一个线程只能有一次 New 和 Terminated 状态,只有处于中间状态才可以相互转换。
【3】想要从 Blocked 状态进入 Runnable 状态,要求线程获取 monitor 锁,而从 Waiting 状态流转到其他状态则比较特殊,因为首先 Waiting 是不限时的,也就是说无论过了多长时间它都不会主动恢复。
【4】处于Waiting状态的线程,如果其他线程调用 notify() 或 notifyAll()来唤醒它,它会直接进入 Blocked 状态,这是为什么呢?因为唤醒 Waiting 线程的线程如果调用 notify() 或 notifyAll(),要求必须首先持有该 monitor 锁,所以处于 Waiting 状态的线程被唤醒时拿不到该锁,就会进入 Blocked 状态,直到执行了 notify()/notifyAll() 的唤醒它的线程执行完毕并释放 monitor 锁,才可能轮到它去抢夺这把锁,如果它能抢到,就会从 Blocked 状态回到 Runnable 状态。
【5】关于是否拥有锁的方面,BLOCKED说明当前线程为获取到锁,WAITING是当前线程已获得锁执行了等待操作所处于的状态,执行等待和唤醒的线程都是基于已经获取锁后的操作