java线程状态研究

2021年06月02日 941次浏览

无争

按照官方的说明java 的thread 有以下几种状态:

  • NEW
  • RUNNABLE
  • BLOCKED
  • WAITING
  • TIMED_WAITING
  • TERMINATED
    会发现通过jstack 打印出来的线程状态不是这样的。

下面这个图是通过IBM 的jca 工具来分析jstack dump文件。顺便说一下jca 是目前发现最好的研究线程栈的工具,本地工具秒杀所有在线分析网站。可以从 https://www.ibm.com/support/pages/ibm-thread-and-monitor-dump-analyzer-java-tmda 下载。但是有一个地方他显示的状态有略微不同,这个下面会讲到。
image.png

这里按照我的理解是java 里的状态是针对java 编码系统的,而通过jstack 打印出来的状态是基于JVM底层的状态来显示的。接下来我们来分别模拟各种状态可能对应的代码,来研究对应真实场景中我们的线程在jvm中的状态。

首先我们需要一段代码来模拟程序的运行时间,这里不能直接使用sleep,因为sleep 本身状态会对线程状态产生干扰。

/**
     * 模拟cpu运行
     *
     * @param duration
     */
    public static void cpuRun(long duration) {

        final long startTime = System.currentTimeMillis();
        int num = 0;
        while (true) {
            num++;
            if (num == Integer.MAX_VALUE) {
                System.out.println(Thread.currentThread() + "rest");
                num = 0;
            }
            if (System.currentTimeMillis() - startTime > duration) {
                return;
            }
        }
    }

Runnable

该状态表示线程具备所有运行条件,在运行队列中准备操作系统的调度,或者正在运行。

New和普通的Runnable太简单了,没啥好讲的

public static void NEW() {
        Thread t = new Thread();
        System.out.println(t.getState());
    }

    public static void RUNNABLE() throws InterruptedException {
        Thread t = new Thread() {

            @Override
            public void run() {
                cpuRun(20000);
            }

        };

        t.start();
        System.out.println(t.getState());
        t.join();

    }


我们研究下,当线程等待io的时候是什么状态

/**
     * 在io 阻塞读的时候线程状态也是runnable的。
     *
     * @throws InterruptedException
     */
    public static void runnableInBlockedIO() throws InterruptedException {
        Scanner in = new Scanner(System.in);

        Thread t1 = new Thread("demo-t1") {

            @Override
            public void run() {
                try {
                    System.out.println("start io read---------");
                    // 命令行中的阻塞读
                    String input = in.nextLine();
                    System.out.println(input);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    IOUtils.closeQuietly(in);
                }
            }
        };
        t1.start();

        t1.join();
    }

下图是打印的线程栈
image.png

可以看到是Runnable ,同样等待socket 连接的时候也一样。

public static void runnableInBlockedSocket() throws InterruptedException {
        Thread serverThread = new Thread(new Runnable() {

            @Override
            public void run() {
                ServerSocket serverSocket = null;
                try {
                    serverSocket = new ServerSocket(10086);
                    while (true) {
                        System.out.println("start socket accept");
                        // 阻塞的accept方法
                        Socket socket = serverSocket.accept();
                        System.out.println("end socket accept");
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    try {
                        serverSocket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "demo-in-socket"); // 线程的名字
        serverThread.start();
        serverThread.join();
    }

image.png

这里没有尝试NIO相关的状态,后面研究netty的时候可以进一步研究下。

waiting for monitor entry

这个状态通常是线程争抢锁,被block住了。

public static void BLOCKED() {

        final Object lock = new Object();

        Runnable run = new Runnable() {

            @Override
            public void run() {
                for (int i = 0; i < Integer.MAX_VALUE; i++) {

                    synchronized (lock) {
                        cpuRun(500);
                        System.out.println(i);
                    }

                }
            }
        };

        Thread t1 = new Thread(run);
        t1.setName("t1");
        Thread t2 = new Thread(run);
        t2.setName("t2");

        t1.start();
        t2.start();

    }

image.png

从线程栈可以看出t1拿到了锁,所以是Runnable,t2没拿到锁所以被Block。

这里需要注意一下,jstack 里显示的是“waiting for monitor entry”,而jca 显示的是“waiting on monitor”,这个跟另一个状态“waiting on condition” 非常像,之前就因为这个问题误判过,非常坑爹。

WAITING

代码中直接调用wait方法。

public static void WAITING() {

    final Object lock = new Object();
    Thread t1 = new Thread() {

        @Override
        public void run() {

            int i = 0;

            while (true) {
                synchronized (lock) {
                    System.out.println("t1 running");
                    cpuRun(2000);
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                    }
                    System.out.println(i++);
                    System.out.println("t1 end");

                }
            }
        }
    };

    Thread t2 = new Thread() {

        @Override
        public void run() {
            while (true) {
                synchronized (lock) {
                    System.out.println("t2 running");
                    cpuRun(5000);
                    lock.notifyAll();
                    System.out.println("t2 end");
                }
                //这里需要一定时间执行,否则t1 可能一直抢不到锁
                cpuRun(100);
            }
        }
    };

    t1.setName("^^t1^^");
    t2.setName("^^t2^^");

    t1.start();
    t2.start();
}

image.png

这里jstack 原文显示的是“WAITING (on object monitor)”,jca中显示“Object.wait()” 略有区别。

waiting on condition

这个状态非常复杂,也是这次动手分析的主要原因。先看下网上的一些官方说法:

该状态出现在线程等待某个条件的发生。具体是什么原因,可以结合 stacktrace来分析。最常见的情况是线程在等待网络的读写,比如当网络数据没有准备好读时,线程处于这种等待状态,而一旦有数据准备好读之后,线程会重新激活,读取并处理数据。如果发现有大量的线程都在处在 Wait on condition,从线程 stack看,正等待网络读写,这可能是一个网络瓶颈的征兆。因为网络阻塞导致线程无法执行。一种情况是网络非常忙,几乎消耗了所有的带宽,仍然有大量数据等待网络读写;另一种情况也可能是网络空闲,但由于路由等问题,导致包无法正常的到达。所以要结合系统的一些性能观察工具来综合分析,比如 netstat统计单位时间的发送包的数目,如果很明显超过了所在网络带宽的限制 ; 观察 cpu的利用率,如果系统态的 CPU时间,相对于用户态的 CPU时间比例较高;如果程序运行在 Solaris 10平台上,可以用 dtrace工具看系统调用的情况,如果观察到 read/write的系统调用的次数或者运行时间遥遥领先;这些都指向由于网络带宽所限导致的网络瓶颈。
另外一种出现 Wait on condition的常见情况是该线程在 sleep,等待 sleep的时间到了时候,将被唤醒。

一句话:具体问题,具体分析。接下来会根据经验尽量模拟遇到的各种场景,但也不一定完全覆盖。

比较简单的sleep

public static void SLEEP() {
        Thread t1 = new Thread("t1") {

            @Override
            public void run() {
                try {
                    Thread.sleep(200000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        };
        t1.start();
    }

image.png

线程池空闲的时候

public static void threadPoolFree() throws InterruptedException {
        System.out.println("all start");
        ThreadPoolExecutor threadPoolExecutor = buildThreadPool();

        Thread.sleep(3000);
        System.out.println("warmup end,start 1 runnable");
        threadPoolExecutor.execute(new Runnable() {

            @Override
            public void run() {
                cpuRun(500000);
                System.out.println("last thread over");
            }
        });
        System.out.println("all end");
    }

这里构造线程池的时候,进行了预热,确保线程池被充分活跃过。

public static ThreadPoolExecutor buildThreadPool() {
        ThreadFactory threadFactory = (new ThreadFactoryBuilder()).setNameFormat("demo-test-%d").build();
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 0, TimeUnit.SECONDS,
                                                                       new LinkedBlockingQueue(1000), threadFactory);

        //这里快速预热一下,让线程池充分初始化
        for (int i = 0; i < 20; i++) {
            threadPoolExecutor.execute(new Runnable() {

                @Override
                public void run() {
                    cpuRun(10);
                }
            });
        }
        return threadPoolExecutor;
    }

image.png

可以看到线程池里5个线程,只有1个是Runnable,其他4个都是Waiting on condition。注意这里具体对账跟上面sleep的区别。核心是在ThreadPoolExecutor.getTask这行上,通过研究过ThreadPoolExecutor的代码可以知道,是在捞队列里面的任务,这个时候park说明队列里面没有任务。很多时候我们线上排查问题遇到比较多的是这个状态,这里可以试一下让线程池繁忙起来的时候,线程池里线程的状态就全是Runnable,所以监控当前线程池的活跃线程数非常重要,它反映了我们系统压力的健康度。这里需要注意threadPoolExecutor.getPoolSize()和threadPoolExecutor.getActiveCount() 的区别,前者是线程池大小,后者是活跃线程数。

这些都是本地模拟的比较简单的情况,实际线上的情况会复杂很多,最终都跟线程池的机制密不可分,线程池的源码分析等有机会再整理下,同时上面没有分析NIO的情况,未完待续...