漫谈Java线程状态

前言

Java语言定义了 6 种线程状态,在任意一个时间点中,一个线程只能只且只有其中的一种状态,并且可以通过特定的方法在不同状态之间进行转换。

今天,我们就详细聊聊这几种状态,以及在什么情况下会发生转换。

一、线程状态

要想知道Java线程都有哪些状态,我们可以直接来看 Thread,它有一个枚举类 State

public class Thread {

    public enum State {

        /**
         * 新建状态
         * 创建后尚未启动的线程
         */
        NEW,

        /**
         * 运行状态
         * 包括正在执行,也可能正在等待操作系统为它分配执行时间
         */
        RUNNABLE,

        /**
         * 阻塞状态
         * 一个线程因为等待临界区的锁被阻塞产生的状态
         */
        BLOCKED,

        /**
         * 无限期等待状态
         * 线程不会被分配处理器执行时间,需要等待其他线程显式唤醒
         */
        WAITING,

        /**
         * 限期等待状态
         * 线程不会被分配处理器执行时间,但也无需等待被其他线程显式唤醒
         * 在一定时间之后,它们会由操作系统自动唤醒
         */
        TIMED_WAITING,

        /**
         * 结束状态
         * 线程退出或已经执行完成
         */
        TERMINATED;
    }
}

二、状态转换

我们说,线程状态并非是一成不变的,可以通过特定的方法在不同状态之间进行转换。那么接下来,我们通过代码,具体来看看这些个状态是怎么形成的。

1、新建

新建状态最为简单,创建一个线程后,尚未启动的时候就处于此种状态。

public static void main(String[] args) {
    Thread thread = new Thread("新建线程");
    System.out.println("线程状态:"+thread.getState());
}
-- 输出:线程状态:NEW

2、运行

可运行线程的状态,当我们调用了start()方法,线程正在Java虚拟机中执行,但它可能正在等待来自操作系统(如处理器)的其他资源。

所以,这里实际上包含了两种状态:Running 和 Ready,统称为 Runnable。这是为什么呢?

这里涉及到一个Java线程调度的问题:

线程调度,是指系统为线程分配处理器使用权的过程。调度主要方式有两种,协同式线程调度和抢占式线程调度。

  • 协同式线程调度

线程的执行时间由线程本身来控制,线程把自己的工作执行完毕之后,要主动通知系统切换到另外一个线程上去。

  • 抢占式线程调度

每个线程将由系统来自动分配执行时间,线程的切换不由线程本身来决定,是基于CPU时间分片的方式。

它们孰优孰劣,不在本文讨论范围之内。我们只需要知道,Java使用的线程调度方式就是抢占式调度。

通常,这个时间分片是很小的,可能只有几毫秒或几十毫秒。所以,线程的实际状态可能会在Running 和 Ready状态之间不断变化。所以,再去区分它们意义不大。

那么,我们再多想一下,如果Java线程调度方式是协同式调度,也许再去区分这两个状态就很有必要了。

public static void main(String[] args) {
    
    Thread thread = new Thread(() -> {
        for (;;){}
    });
    thread.start();
    System.out.println("线程状态:"+thread.getState());
}
-- 输出:线程状态:RUNNABLE

简单来看,上面的代码就使线程处于Runnable状态。但值得我们注意的是,如果一个线程在等待阻塞I/O的操作时,它的状态也是Runnable的。

我们来看两个经典阻塞IO的例子:

public static void main(String[] args) throws Exception {

    Thread t1 = new Thread(() -> {
        try {
            ServerSocket serverSocket = new ServerSocket(9999);
            while (true){
                Socket socket = serverSocket.accept();
                OutputStream outputStream = socket.getOutputStream();
                outputStream.write("Hello".getBytes());
                outputStream.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    },"accept");
    t1.start();

    Thread t2 = new Thread(() -> {
        try {
            Socket socket = new Socket("127.0.0.1",9999);
            for (;;){
                InputStream inputStream = socket.getInputStream();
                byte[] bytes = new byte[5];
                inputStream.read(bytes);
                System.out.println(new String(bytes));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    },"read");
    t2.start();
}

上面的代码中,我们知道,serverSocket.accept()inputStream.read(bytes);都是阻塞式方法。

它们一个在等待客户端的连接;一个在等待数据的到来。但是,这两个线程的状态却是 RUNNABLE的。

"read" #13 prio=5 os_prio=0 tid=0x0000000023f6c800 nid=0x1cd0 runnable [0x0000000024b3e000]
   java.lang.Thread.State: RUNNABLE
    at java.net.SocketInputStream.socketRead0(Native Method)
    at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
    at java.net.SocketInputStream.read(SocketInputStream.java:171)
    at java.net.SocketInputStream.read(SocketInputStream.java:141)
"accept" #12 prio=5 os_prio=0 tid=0x0000000023f68000 nid=0x4cec runnable [0x0000000024a3e000]
   java.lang.Thread.State: RUNNABLE
    at java.net.DualStackPlainSocketImpl.accept0(Native Method)
    at java.net.DualStackPlainSocketImpl.socketAccept(DualStackPlainSocketImpl.java:131)
    at java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:409)
    at java.net.PlainSocketImpl.accept(PlainSocketImpl.java:199)

这又是为什么呢 ?

我们前面说过,处于 Runnable 状态下的线程,正在 Java 虚拟机中执行,但它可能正在等待来自操作系统(如处理器)的其他资源

不管是CPU、网卡还是硬盘,这些都是操作系统的资源而已。当进行阻塞式的IO操作时,或许底层的操作系统线程确实处在阻塞状态,但在这里我们的 Java 虚拟机线程的状态还是 Runnable

不要小看这个问题,很具有迷惑性。有些面试官如果问到,如果一个线程正在进行阻塞式 I/O 操作时,它处于什么状态?是Blocked还是Waiting?

那这时候,我们就要义正言辞的告诉他:亲,都不是哦~

3、无限期等待

处于无限期等待状态下的线程,不会被分配处理器执行时间,除非其他线程显式的唤醒它。

最简单的场景就是调用了 Object.wait() 方法。

public static void main(String[] args) throws Exception {

    Object object = new Object();
    new Thread(() -> {
        synchronized (object){
        try {
            object.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }}).start();
}
-- 输出:线程状态:WAITING

此时这个线程就处于无限期等待状态,除非有别的线程显式的调用object.notifyAll();来唤醒它。

然后,就是Thread.join()方法,当主线程调用了此方法,就必须等待子线程结束之后才能继续进行。

public static void main(String[] args) throws Exception {

    Thread mainThread = new Thread(() -> {
        Thread subThread = new Thread(() -> {
            for (;;){}
        });
        subThread.start();
        try {
            subThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    mainThread.start();
    System.out.println("线程状态:"+thread.getState());
}
//输出:线程状态:WAITING

如上代码,在主线程 mainThread 中调用了子线程的join()方法,那么主线程就要等待子线程结束运行。所以此时主线程mainThread的状态就是无限期等待。
多说一句,其实join()方法内部,调用的也是Object.wait()

最后,我们说说LockSupport.park()方法,它同样会使线程进入无限期等待状态。也许有的朋友对它很陌生,没有用过,我们来看一个阻塞队列的例子。

public static void main(String[] args) throws Exception {

    ArrayBlockingQueue<Long> queue = new ArrayBlockingQueue(1);
    Thread thread = new Thread(() -> {
        while (true){
            try {
                queue.put(System.currentTimeMillis());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    });
    thread.start();
}

如上代码,往往我们会通过阻塞队列的方式来做生产者-消费者模型的代码。

这里,ArrayBlockingQueue长度为1,当我们第二次往里面添加数据的时候,发现队列已满,线程就会等待这里,它的源码里面正是调用了LockSupport.park()

同样的,这里也比较具有迷惑性,我来问你:阻塞队列中,如果队列为空或者队列已满,这时候执行take或者put操作的时候,线程的状态是 Blocked 吗?

那这时候,我们需要谨记这里的线程状态还是 WAITING。它们之间的区别和联系,我们后文再看。

4、限期等待

同样的,处于限期等待状态下的线程,也不会被分配处理器执行时间,但是它在一定时间之后可以自动的被操作系统唤醒。

这个跟无限期等待的区别,仅仅就是有没有带有超时时间参数。

比如:

object.wait(3000);
thread.join(3000);
LockSupport.parkNanos(5000000L);
Thread.sleep(1000);

像这种操作,都会使线程处于限期等待的状态 TIMED_WAITING。因为Thread.sleep()必须带有时间参数,所以它不在无限期等待行列中。

5、阻塞

一个线程因为等待临界区的锁被阻塞产生的状态,也就是说,阻塞状态的产生是因为它正在等待着获取一个排它锁。

这里,我们来看一个 synchronized的例子。

public static void main(String[] args) throws Exception {

    Object object = new Object();
    Thread t1 = new Thread(() -> {
        synchronized (object){
            for (;;){}
        }
    });
    t1.start();

    Thread t2 = new Thread(() -> {
        synchronized (object){
            System.out.println("获取到object锁,线程执行。");
        }
    });
    t2.start();
    System.out.println("线程状态:"+t2.getState());
}
//输出:线程状态:BLOCKED

我们看上面的代码,object对象锁一直被线程 t1 持有,所以线程 t2 的状态一直会是阻塞状态。

我们接着再来看一个锁的例子:

public static void main(String[] args){

    Lock lock = new ReentrantLock();
    lock.lock();
    Thread t1 = new Thread(() -> {
        lock.lock();
        System.out.println("已获取lock锁,线程执行");
        lock.unlock();
    });
    t1.start();
    System.out.println("线程状态:"+t1.getState());
}

如上代码,我们有一个ReentrantLock,main线程已经持有了这个锁,t1 线程会一直等待在lock.lock();

那么,此时 t1 线程的状态是什么呢 ?

其实答案是WAITING,即无限期等待状态。这又是为什么呢 ?

原因在于,Lock接口是Java API实现的锁,它的底层实现其实是抽象同步队列,简称AQS

在通过lock.lock()获取锁的时候,如果锁正在被其他线程持有,那么线程会被放入AQS队列后,阻塞挂起。

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        如果tryAcquire返回false,会把当前线程放入AQS阻塞队列
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

acquireQueued方法会将当前线程放入 AQS 阻塞队列,然后调用LockSupport.park(this);挂起线程。

所以,这也就解释了为什么lock.lock()获取锁的时候,当前的线程状态会是 WAITING

常常有人会问,synchronized和Lock的区别,除了一般性的答案,此时你也可以说一下线程状态的差异,我猜可能很少有人会意识到这一点。

6、结束

一个线程,当它退出或已经执行完成的时候,就是结束状态。

public static void main(String[] args) throws Exception {
    
    Thread thread = new Thread(() -> System.out.println("线程已执行"));
    thread.start();
    Thread.sleep(1000);
    System.out.println("线程状态:"+thread.getState());
}
//输出:  线程已执行
线程状态:TERMINATED

三、总结

本文介绍了 Java 线程的不同状态,以及在何种情况下发生转换。

image

原创不易,客官们点个赞再走嘛,这将是笔者持续写作的动力~

image

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272