(Java并发基础)线程的5种状态及转换

从下图可知,线程有5种状态。如果想要获取线程的状态,可以通过getState()方法获取。线程在任何时刻只能有一种状态。

New状态

New表示线程被创建但尚未启动的状态:当我们用new Thread()新建一个线程时,如果线程没有开始运行start()方法,所以也没有开始执行run()方法里的代码,那么此时它的状态就是New,此时仅由JVM为其分配内存,并初始化其成员变量的值,一旦调用Thread.start()方法,则会从New变成Runnable。就是下图中的白色区域。

Runnable可运行

Java中的Runnable状态分别对应操作系统线程状态中的两种状态,分别是运行和就绪状态。也就是线程可能正在等待分配CPU资源或者当前线程从CPU切出变到就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。

阻塞状态

阻塞状态实际上包括三种状态,分别是Blocked(被阻塞)、Waiting(等待)、Timed Waiting(计时等待)。如图左边的红色是等待阻塞,下边的红色的是被阻塞,右边的粉红色是超时等待。

Blocked被阻塞

从图上可知,从Runnable状态进入Blocked状态只有一种可能,就是进入了synchronized的方法或者代码块时没有抢到monitor锁。而从Blocked状态回到Runnable状态表示抢到了monitor锁。

Waiting等待

线程从Runnable状态到Waiting状态有三种可能。

  1. 没有设置 Timeout 参数的 Object.wait() 方法。
  2. 没有设置 Timeout 参数的 Thread.join() 方法。
  3. LockSupport.park() 方法。

Blocked被阻塞仅仅针对synchronized monitor锁,可能在Java 中还有很多其他的锁,比如ReentrantLock,因为ReentrantLock本质上是调用了LockSupport.park()方法,所以没抢到锁就会进入到Waiting状态。同样,Object.wait()和Thread.join()也会让线程进入Waiting状态。

在HotSpot虚拟机中,monitor采用ObjectMonitor实现,每个线程都具有两个队列,分别为free和used,用来存放ObjectMonitor。如果当前free列表为空,线程将向全局global list请求分配ObjectMonitor。

ObjectMonitor对象中有两个队列,都用来保存ObjectWaiter对象,分别是_WaitSet 和 _EntrySet。_owner用来指向获得ObjectMonitor对象的线程。

ObjectWaiter对象是双向链表结构,保存了_thread(当前线程)以及当前的状态TState等数据, 每个等待锁的线程都会被封装ObjectWaiter对象。

在这里插入图片描述
_WaitSet :处于wait状态的线程,会被加入到wait set;

_EntrySett:处于等待锁block状态的线程,会被加入到entry set;

wait方法实现:
lock.wait()方法最终通过ObjectMonitor的void wait(jlong millis, bool interruptable, TRAPS);实现
1、将当前线程封装成ObjectWaiter对象node
2、通过ObjectMonitor::AddWaiter方法将node添加到_WaitSet列表中
3、通过ObjectMonitor::exit方法释放当前的ObjectMonitor对象,这样其它竞争线程就可以获取该ObjectMonitor对象
4、最终底层的park方法会挂起线程

ObjectSynchorizer::wait方法通过Object对象找到ObjectMonitor对象调用方法void ObjectMonitor::wait(),通过ObjectMonitor::AddWaiter调用把新建里的ObjectWaiter对象,放入到_WaitSet队列的末尾然后,在ObjectMonitor::exit释放锁,接着thread_ParkEvent->park,也就是进行wait。

Blocked 与 Waiting 的区别是 Blocked 在等待其他线程释放 monitor 锁,而 Waiting 则是在等待某个条件,比如 join 的线程执行完毕,或者是 notify()/notifyAll() 。

Timed Waiting超时等待

超时等待和等待阻塞这两个状态是非常相似的,区别仅在于有没有时间限制,超时等待会等待超时,由系统自动唤醒,或者在超时前被唤醒信号唤醒。
以下情况会让线程进入超时等待状态。

  1. 设置了时间参数的 Object.wait(long timeout) 方法;
  2. 设置了时间参数的 Thread.sleep(long millis) 方法;
  3. 设置了时间参数的 Thread.join(long millis) 方法;
  4. 设置了时间参数的 LockSupport.parkNanos(long nanos) 方法和 LockSupport.parkUntil(long deadline) 方法。

而从超时等待进入到Runnable状态,要求线程获取monitor锁,而从Waiting状态流转到其他状态则比较特殊,因为首先Waiting是不限时的,因为它本身是不知道要等待多久的。只要以下几种方式被其他线程唤醒。

  1. 只有当执行了LockSupport.unpark(),或者join的线程运行结束,或者被中断时才可以进入Runnable状态。
  2. 如果其他线程调用 notify() 或 notifyAll()来唤醒它,它会直接进入 Blocked 状态,这是为什么呢?因为唤醒 Waiting 线程的线程如果调用 notify() 或 notifyAll(),要求必须首先持有该 monitor 锁,所以处于 Waiting 状态的线程被唤醒时拿不到该锁,就会进入 Blocked 状态,直到执行了 notify()/notifyAll() 的唤醒它的线程执行完毕并释放 monitor 锁,才可能轮到它去抢夺这把锁,如果它能抢到,就会从 Blocked 状态回到 Runnable 状态。(图上有点问题)

同样在 Timed Waiting 中执行 notify() 和 notifyAll() 也是一样的道理,它们会先进入 Blocked 状态,然后抢夺锁成功后,再回到 Runnable 状态。

对于超时等待而言,如果它的超时时间到了且能直接获取到锁/join的线程运行结束/被中断/调用了LockSupport.unpark()方法,会直接恢复到Runnable状态,而无须经历Blocked状态。

Terminated终止

进入该状态有两种可能。

  1. run()或call()方法执行完毕,线程正常退出。
  2. 出现一个没有捕获的异常或Error,终止了run()方法,最终导致意外终止。
  3. 直接调用该线程的stop()方法来结束该线程,该方法通常容器导致死锁,不推荐使用。

更多时间,我们是使用使用退出标志退出线程,如有些线程是伺服线程。它们需要长时间的运行,只有在外部某些条件满足的情况下,才能关闭这些线程。使用一个变量来控制循环,例如:最直接的方法就是设一个 boolean 类型的标志,并通过设置这个标志为 true 或 false 来控制 while循环是否退出。

public class ThreadSafe extends Thread {
 public volatile boolean exit = false;
 public void run() {
 while (!exit){
 //do something
 }
 }
}

定义了一个退出标志 exit,当 exit 为 true 时,while 循环退出,exit 的默认值为 false.在定义 exit时,使用了一个 Java 关键字 volatile,这个关键字的目的是使 exit 同步,也就是说在同一时刻只能由一个线程来修改 exit 的值。

interrupt方法结束线程

使用interrupt()方法来中断线程有两种情况:

  1. 线程处于阻塞状态:如使用了sleep,同步锁的wait,socket 中的receiver,accept 等方法时,会使线程处于阻塞状态。当调用线程的interrupt()方法时,会抛出InterruptException 异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后break 跳出循环状态,从而让我们有机会结束这个线程的执行。通常很多人认为只要调用interrupt 方法线程就会结束,实际上是错的, 一定要先捕获InterruptedException 异常之后通过break 来跳出循环,才能正常结束run 方法
  2. 线程未处于阻塞状态:使用isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置true,和使用自定义的标志来控制循环是一样的道理。
public class ThreadSafe extends Thread {
public void run() {
while (!isInterrupted()){ //非阻塞过程中通过判断中断标志来退出
try{
   Thread.sleep(5*1000);//阻塞过程捕获中断异常来退出
}catch(InterruptedException e){
   e.printStackTrace();
   break;//捕获到异常之后,执行break 跳出循环
 }
 }
 }
}

stop方法终止线程(线程不安全)

程序中可以直接使用thread.stop()来强行终止线程,但是stop方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出ThreadDeatherror 的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用stop 方法来终止线程。

sleep与wait的区别

  1. 对于sleep()方法,我们首先要知道该方法是属于Thread 类中的。而wait()方法,则是属于Object 类中的。
  2. sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。
  3. 在调用sleep()方法的过程中,线程不会释放对象锁。
  4. 而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

注意点

最后我们再看线程转换的两个注意点。
线程的状态是需要按照箭头方向来走的,比如线程从 New 状态是不可以直接进入 Blocked 状态的,它需要先经历 Runnable 状态。

线程生命周期不可逆:一旦进入 Runnable 状态就不能回到 New 状态;一旦被终止就不可能再有任何状态的变化。所以一个线程只能有一次 New 和 Terminated 状态,只有处于中间状态才可以相互转换。

在这里插入图片描述

start与run的区别

  1. start()方法来启动线程,真正实现了多线程运行。这时无需等待run 方法体代码执行完毕,可以直接继续执行下面的代码。
  2. 通过调用Thread 类的start()方法来启动一个线程, 这时此线程是处于就绪状态,并没有运行。
  3. 方法run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行run 函数当中的代码。 Run 方法运行结束, 此线程终止。然后CPU再调度其它线程。

参考:
java的Object里wait()实现原理 https://www.pianshen.com/article/25761502569/

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值