一宿君

V1

2022/08/06阅读:18主题:默认主题

Java并发编程系列(三)线程的六种状态及上下文切换

上期内容:Java并发编程系列(二)线程组、线程优先级以及守护线程

本期内容包含以下几点:

  • 操作系统中的线程状态
  • Java中线程的六个状态
  • 线程状态的切换

操作系统线程主要有以下三个状态:

  • 就绪状态(ready):线程正在等待使用CPU,经调度程序调用之后可进入running状态。
  • 运行状态(running):线程正在使用CPU。
  • 等待状态(waiting):线程经过事件的调用或者正在等待其他资源(如I/O)。
操作系统线程状态
操作系统线程状态

在现在的操作系统中,线程是被视为轻量级进程的,所以操作系统线程的状态其实和操作系统进程的状态是一致的

Java中线程的六个状态:

状态名称 说明
NEW 初始化状态,线程被创建,但是还没有调用start方法
RUNNABLE 运行状态,Java线程将操作系统中的就绪和运行两种状态笼统的称为“运行中”
BLOCKED 阻塞状态,表示线程阻塞于锁
WAITING 等待状态,表示线程进入等待状态,进入该状态需要其他线程做出一些特定的动作(通知或中断)
TIME_WAITING 超时等待状态,进入该状态,线程在等待指定时间后自动返回(唤醒)
TERMINATED 终止状态,标识当前线程已经执行完毕
// Thread.State 源码,分别对应上述表格中的状态
public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

1、NEW状态

处于NEW状态的线程此时尚未启动,这里的尚未启动指的是还没调用Thread实例的start()方法。

private static void testStateNew() {
    Thread thread = new Thread(() -> {});
    System.out.println(thread.getState()); // 输出 NEW
}

关于start()的两个引申问题(具有代表性)

  1. 反复调用同一个线程的start()方法是否可行?
  2. 假如一个线程执行完毕(此时处于TERMINATED状态),再次调用这个线程的start()方法是否可行?

要分析这两个问题,我们先来看看start()的源码:

public synchronized void start() {
    if (threadStatus != 0)//如果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) {

        }
    }
}

从上述源码中我们可以看到,在start()内部,这里有一个threadStatus的变量。如果它不等于0,调用start()是会直接抛出异常的。

private static void testStateNew() {
        Thread thread = new Thread(() -> {});
        System.out.println(thread.getState()); // 输出 NEW
        thread.start();//第一次调用
        thread.start();//第二次调用
    }

输出结果:

NEW
Exception in thread "main" java.lang.IllegalThreadStateException
 at java.lang.Thread.start(Thread.java:708)
 at com.wbs.anightmonarch.concurrent.ThreadState.testStateNew(ThreadState.java:20)
 at com.wbs.anightmonarch.concurrent.ThreadState.main(ThreadState.java:12)

我们在连续两次调用start方法后,报出异常,是什么原因导致的,下面我们debug追踪一下:

start-debug-1
start-debug-1

我在start()方法内部第一行if这打断点,第一次执行到start方法时,可以看到threadStatus的值为0,接着放行第二次调用start方法:

start-debug-2
start-debug-2

可以看到上述两次调用start方法:

  • 第一次调用时threadStatus的值是0。
  • 第二次调用时threadStatus的值不为0。

start0()方法并没有改变threadStatus的值,那我们看下线程此时的状态源码:

// Thread.getState方法源码:
public State getState() {
    // get current thread state
    return sun.misc.VM.toThreadState(threadStatus);
}

// sun.misc.VM 源码:
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;
    }
}

所以,我们结合上面的源码可以得到引申的两个问题的结果:

两个问题的答案都是不可行,在调用一次start()之后,threadStatus的值会改变(threadStatus !=0),此时再次调用start()方法会抛出IllegalThreadStateException异常。

比如,threadStatus为2代表当前线程状态为TERMINATED。

2、RUNNABLE状态

Thread源码里对RUNNABLE状态的定义:

/**
 * Thread state for a runnable thread.  A thread in the runnable
 * state is executing in the Java virtual machine but it may
 * be waiting for other resources from the operating system
 * such as processor.
 */


释义:
可运行线程的线程状态。处于可运行状态的线程正在 Java 虚拟机中执行,但它可能正在等待来自操作系统CPU的其他资源,例如处理器。

Java线程的RUNNABLE状态其实是包括了传统操作系统线程的readyrunning两个状态的。

3、BLOCKED状态

阻塞状态。处于BLOCKED状态的线程正等待锁的释放以进入同步区。

我们用BLOCKED状态举个生活中的例子:

假如今天你下班后准备去食堂吃饭。你来到食堂仅有的一个窗口,发现前面已经有个人在窗口前了,此时你必须得等前面的人从窗口离开才行。

假设你是线程t2,你前面的那个人是线程t1。此时t1占有了锁(食堂唯一的窗口),你t2正在等待锁(窗口)的释放,所以此时你t2就处于BLOCKED状态。

4、WAITING状态

等待状态。处于等待状态的线程想要变成RUNNABLE状态需要其他线程唤醒。

调用如下方法会使线程进入等待状态:

  • Object.wait():使当前线程处于等待状态直到另一个线程唤醒它;
  • Thread.join():等待线程执行完毕,其实底层调用的也是Object实例的wait方法;
  • LockSupport.park():除非获得调用许可,否则禁用当前线程进行线程调度。

执行wait方法之后,线程进入等待状态,进入等待状态的线程需要其他线程的通知(notify()、notifyAll()……)等方法唤醒才能够回到Runnable运行状态,而超时等待状态相当于在等待状态的基础上增加了超时限制,也就是超时时间到达时将会返回到运行状态。

解除WAITINTG等待状态:

  • Object.notify():唤醒一个等待线程
  • Object.notifyAll():唤醒所有的等待线程
  • LockSupport.unpark(Thread):唤醒指定的等待线程

5、TIMED_WAITING状态

超时等待状态。线程等待一个具体的时间,时间到后会被自动唤醒。

调用如下方法会使线程进入超时等待状态:

  • Thread.sleep(long millis):使当前线程睡眠指定时间;
  • Object.wait(long timeout):线程休眠指定时间,等待期间可以通过notify()/notifyAll()唤醒;
  • Thread.join(long millis):等待当前线程最多执行millis毫秒,如果millis为0,则会一直执行;
  • LockSupport.parkNanos(long nanos): 除非获得调用许可,否则禁用当前线程进行线程调度指定时间;
  • LockSupport.parkUntil(long deadline):同上,也是禁止线程进行调度指定时间;

解除TIMED_WAITINTG超时等待状态:k

  • Object.notify():唤醒一个超时等待线程
  • Object.notifyAll():唤醒所有的超时等待线程
  • LockSupport.unpark(Thread):唤醒指定的超时等待线程

6、TERMINATED终止状态

终止状态。此时线程已执行完毕,进入这个状态有两种方式:

  • run()方法执行完毕,线程正常退出;
  • 出现一个没有捕获的异常,终止了run()方法,最终导致意外终止。

线程的状态切换

先看下Java中线程切换的流程图(重点理解):

线程状态切换流程图
线程状态切换流程图

BlockedWaiting的区别,以及进入如何进入Runnable状态:

  • 线程在进入synchronized同步代码块时,并没有获取到monitor同步锁,此时就处于同步阻塞状态;同时我们应该了解到。关于synchronized同步代码块都是基于monitor锁实现的。
  • Blocked阻塞状态是在等待获取其他线程释放monitor锁,从而进入Runnable状态;

这里需要明确指出一点大部分所认为的关于Waiting等待状态错误看法:

  • 我们知道关于wait()和notify()/notifyAll()等方法,只能在synchronized同步代码块中才能调用,在外面调用则会报出异常
  • 那也就是说,其他线程通过调用notify()/notifyAll()等方法来唤醒当前处于Waiting状态的线程,因为当前线程是处于synchronized代码块中,所以被唤醒后就进入到了Blocked阻塞状态,等到获取到monitor锁,才能够进入Runnable状态;
  • 如果处于Waiting/Timed_Waiting状态的线程想直接进入到Runnable状态,就需要其他join程序执行结束或者被中断,或者执行LockSupport.unpark()方法,可以直接进入Runnable状态。

其实只要翻看一下 jdk 文档就知道了。

/**
 * A thread in the blocked state is waiting for a monitor lock
 * to enter a synchronized block/method or
 * reenter a synchronized block/method after calling
 * {@link Object#wait() Object.wait}.
 */

BLOCKED,

翻译:

/**
 * 在如下场景下等待一个锁(获取锁失败)
 * 1. 进入 synchronized 方法
 * 2. 进入 synchronized 块
 * 3. 调用 wait 后(被 notify)重新进入 synchronized 方法/块
 */

BLOCKED,

当一个阻塞在 wait 的线程,被另一个线程 notify 后,重新进入 synchronized 区域,此时需要重新获取锁,如果失败了,就变成 BLOCKED 状态。

错误的理解图:Blocked和Waiting进入Runnable状态
错误的理解图:Blocked和Waiting进入Runnable状态

乍一看上面说的没毛病,

但是!但是!但是!

不可认为从wait状态被notify后是直接进入到Blocked状态的!!!实际上,从wait状态被notify之后,首先是进入到Runnable状态,等待 CPU 时间片分配,等分配到时间片时,才有机会去尝试获取锁,如果获取锁成功,会直接进入到Running状态,如果获取锁失败,这个时候才会从Runnable状态进入到Blocked状态。

正确的理解图:wait到Blocked状态的转化过程
正确的理解图:wait到Blocked状态的转化过程

只要把上述流程图搞清楚,我们在面对业务场景的时候就会很清晰怎么做,为了更加深刻的了解线程状态的转换,下面我们做一个简单的业务场景Demo。

示例业务场景Demo

实现一个容器, 提供两个方法add() , size(), 需要写两个线程:

  • 线程1: 添加10个元素到容器中, 得全部执行完。
  • 线程2: 实现监控元素的个数, 当个数到5个时, 打印"监控结束" , 线程2结束,让线程1继续执行到结束。

第一种方式:wait()、notify()和synchronized()方式实现

public class TwoThreadSynchronizedWaitNotifyDemo {

    private static List list = new LinkedList();

    private static void add(Object o){
        list.add(o);
    }

    private static int size(){
        return list.size();
    }

    public static void main(String[] args) {
        //创建一个锁对象
        Object lock = new Object();

        Thread addThread = new Thread(){
            @Override
            public void run() {
                super.run();
                synchronized (lock){//获取锁
                    for (int i = 1; i <= 10; i++) {
                        add(new Object());
                        System.out.println("添加到第" + i + "个对象");
                        if(i==5){
                            lock.notify();//2、此时通知sizeThread从等待状态进入阻塞状态,当前addThread线程并没有释放锁,sizeThread仍然还得不到锁
                            try {
                                lock.wait();//3、上面notify只是通知sizeThread线程做好准备,此处是让自己本身立即释放锁,并进入等待状态(等待阻塞),让sizeThread先执行一下
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
        };

        Thread sizeThread = new Thread(){
            @Override
            public void run() {
                super.run();
                synchronized (lock){//获取锁
                    try {
                        lock.wait();//1、立即释放锁,进入等待状态(等待阻塞)
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("size==" + size() + ",监控结束。");
                    lock.notify();//4、此处通知addThread线程从等待状态进入阻塞状态,随后当前线程执行完成释放锁,让addThread线程获取到锁,继续执行
                }
            }
        };

        sizeThread.start();//0、必须sizeThread线程先执行,否则系统会因为sizeThrea没有得到通知而永久陷入阻塞
        addThread.start();
    }
}

输出结果:

添加到第1个对象
添加到第2个对象
添加到第3个对象
添加到第4个对象
添加到第5个对象
size==5,监控结束。
添加到第6个对象
添加到第7个对象
添加到第8个对象
添加到第9个对象
添加到第10个对象

思路:

  • 0、必须sizeThread线程先执行,否则系统会因为sizeThrea没有得到通知而永久陷入阻塞
  • 1、sizeThread线程立即释放锁,进入等待状态(等待阻塞)
  • 2、addThread线程此时通知sizeThread从等待状态进入阻塞状态,当前addThread线程并没有释放锁,sizeThread仍然还得不到锁
  • 3、上面notify只是通知sizeThread线程做好准备,此处是让自己本身立即释放锁,并进入等待状态(等待阻塞),让sizeThread先执行一下
  • 4、sizeThread线程此处通知addThread线程从等待状态进入阻塞状态,随后当前线程执行完成释放锁,让addThread线程获取到锁,继续执行

第二种方式:LockSupport.park()和LockSupport.unpark()方式实现

public class LockSupportDemo {

    private static List list = new LinkedList();

    private static void add(Object o){
        list.add(o);
    }

    private static int size(){
        return list.size();
    }


    static Thread addThread = null;
    static Thread sizeThread = null;

    public static void main(String[] args) {
        //创建一个锁对象
        Object lock = new Object();

        addThread = new Thread(){
            @Override
            public void run() {
                super.run();
                for (int i = 1; i <= 10; i++) {
                    add(new Object());
                    System.out.println("添加到第" + i + "个对象");
                    if(i==5){
                        LockSupport.unpark(sizeThread);//2、此时将sizeThread等待队列中唤醒到Runnable状态就绪,当前addThread线程并没有释放锁,sizeThread仍然还得不到锁
                        LockSupport.park();//3、上面unpark只是通知sizeThread线程做好准备,此处才是让自己本身立即进入等待状态(等待阻塞),并释放锁,让sizeThread获取锁先执行一下
                    }
                }
            }
        };

        sizeThread = new Thread(){
            @Override
            public void run() {
                super.run();
                LockSupport.park();//1、释放锁,并进入到Runnable就绪状态
                System.out.println("size==" + size() + ",监控结束。");
                LockSupport.unpark(addThread);//4、此处让addThread线程获取到锁,并从等待状态进入Runnable状态,继续执行
            }
        };

        sizeThread.start();//0、必须sizeThread线程先执行,否则系统会因为sizeThread没有得到通知而永久陷入阻塞
        addThread.start();
    }
}

输出结果:

添加到第1个对象
添加到第2个对象
添加到第3个对象
添加到第4个对象
添加到第5个对象
size==5,监控结束。
添加到第6个对象
添加到第7个对象
添加到第8个对象
添加到第9个对象
添加到第10个对象

思路:J

  • 0、必须sizeThread线程先执行,否则系统会因为sizeThread没有得到通知而永久陷入阻塞
  • 1、sizeThreadx线程释放锁,并进入到Runnable就绪状态
  • 2、addThread线程此时将sizeThread等待队列中唤醒到Runnable状态就绪,当前addThread线程并没有释放锁,sizeThread仍然还得不到锁
  • 3、上面unpark只是通知sizeThread线程做好准备,此处才是让自己本身立即进入等待状态(等待阻塞),并释放锁,让sizeThread获取锁先执行一下

扩展问题:为什么notify(), wait()等函数定义在Object中,而不是Thread中

Object中的wait()/notify()方法和synchronized关键字一样,都是对对象的同步锁操作,见上述示例1所示。

wait()会让当前线程等待,因为进入等待状态,所以会释放当前所持有的同步锁monitor,如果不释放其他线程就获取不到锁就永远无法运行,这是底层操作系统的规定!!!

根据前面的介绍,我们知道处于等待状态的线程,可以通过notify()notifyAll()等方法被唤醒,那现在请思考一个问题:notify()是依据什么唤醒等待线程的?或者说,wait()等待线程和notify()之间是通过什么关联起来的?

答案是:对象的同步锁

负责唤醒等待线程的那个线程,我们称其为唤醒线程,它只有在获取对象的同步锁,此处的同步锁和处于等待状态线程的同步锁同一个,并且调用notify()或者notifyAll()方法之后,才能唤醒等待线程。注意:此时虽然等待线程被唤醒了,但是它还不能立即执行,因为唤醒线程还持有对象的同步锁,所以必须等唤醒线程释放了对象的同步锁,等待线程才能获取到对象的同步锁进而继续执行。

总之,notify()、notifyAll()和wait()等方法都依赖于同步锁,而同步锁是对象所持有的,并且每个对象有且仅有一个,这就是为什么notify()、notifyAll()和wait()等方法定义在Object类中,而不是Thread类中的原因。


内容总结来源以下文章:


专注于Java基础、进阶、面试以及计算机基础知识分享🐳。偶尔认知思考、日常水文🐌。
公众号二维码
公众号二维码
随和的皮蛋桑
随和的皮蛋桑

分类:

后端

标签:

后端

作者介绍

一宿君
V1

Java开发工程师