按照官方的说明java 的thread 有以下几种状态:
NEWRUNNABLEBLOCKEDWAITINGTIMED_WAITINGTERMINATED会发现通过jstack 打印出来的线程状态不是这样的。
下面这个图是通过IBM 的jca 工具来分析jstack dump文件。顺便说一下jca 是目前发现最好的研究线程栈的工具,本地工具秒杀所有在线分析网站。可以从 https://www.ibm.com/support/pages/ibm-thread-and-monitor-dump-analyzer-java-tmda 下载。但是有一个地方他显示的状态有略微不同,这个下面会讲到。
这里按照我的理解是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; } } }该状态表示线程具备所有运行条件,在运行队列中准备操作系统的调度,或者正在运行。
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(); }下图是打印的线程栈
可以看到是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(); }这里没有尝试NIO相关的状态,后面研究netty的时候可以进一步研究下。
这个状态通常是线程争抢锁,被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(); }从线程栈可以看出t1拿到了锁,所以是Runnable,t2没拿到锁所以被Block。
这里需要注意一下,jstack 里显示的是“waiting for monitor entry”,而jca 显示的是“waiting on monitor”,这个跟另一个状态“waiting on condition” 非常像,之前就因为这个问题误判过,非常坑爹。
代码中直接调用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(); }这里jstack 原文显示的是“WAITING (on object monitor)”,jca中显示“Object.wait()” 略有区别。
这个状态非常复杂,也是这次动手分析的主要原因。先看下网上的一些官方说法:
该状态出现在线程等待某个条件的发生。具体是什么原因,可以结合 stacktrace来分析。最常见的情况是线程在等待网络的读写,比如当网络数据没有准备好读时,线程处于这种等待状态,而一旦有数据准备好读之后,线程会重新激活,读取并处理数据。如果发现有大量的线程都在处在 Wait on condition,从线程 stack看,正等待网络读写,这可能是一个网络瓶颈的征兆。因为网络阻塞导致线程无法执行。一种情况是网络非常忙,几乎消耗了所有的带宽,仍然有大量数据等待网络读写;另一种情况也可能是网络空闲,但由于路由等问题,导致包无法正常的到达。所以要结合系统的一些性能观察工具来综合分析,比如 netstat统计单位时间的发送包的数目,如果很明显超过了所在网络带宽的限制 ; 观察 cpu的利用率,如果系统态的 CPU时间,相对于用户态的 CPU时间比例较高;如果程序运行在 Solaris 10平台上,可以用 dtrace工具看系统调用的情况,如果观察到 read/write的系统调用的次数或者运行时间遥遥领先;这些都指向由于网络带宽所限导致的网络瓶颈。 另外一种出现 Wait on condition的常见情况是该线程在 sleep,等待 sleep的时间到了时候,将被唤醒。
一句话具体问题,具体分析。接下来会根据经验尽量模拟遇到的各种场景,但也不一定完全覆盖。
这里构造线程池的时候,进行了预热,确保线程池被充分活跃过。
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; }可以看到线程池里5个线程,只有1个是Runnable,其他4个都是Waiting on condition。注意这里具体对账跟上面sleep的区别。核心是在ThreadPoolExecutor.getTask这行上,通过研究过ThreadPoolExecutor的代码可以知道,是在捞队列里面的任务,这个时候park说明队列里面没有任务。很多时候我们线上排查问题遇到比较多的是这个状态,这里可以试一下让线程池繁忙起来的时候,线程池里线程的状态就全是Runnable,所以监控当前线程池的活跃线程数非常重要,它反映了我们系统压力的健康度。这里需要注意threadPoolExecutor.getPoolSize()和threadPoolExecutor.getActiveCount() 的区别,前者是线程池大小,后者是活跃线程数。
这些都是本地模拟的比较简单的情况,实际线上的情况会复杂很多,最终都跟线程池的机制密不可分,线程池的源码分析等有机会再整理下,同时上面没有分析NIO的情况,未完待续...
