无争
按照官方的说明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 下载。但是有一个地方他显示的状态有略微不同,这个下面会讲到。
这里按照我的理解是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();
}
下图是打印的线程栈
可以看到是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的时候可以进一步研究下。
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();
}
从线程栈可以看出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();
}
这里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();
}
线程池空闲的时候
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;
}
可以看到线程池里5个线程,只有1个是Runnable,其他4个都是Waiting on condition。注意这里具体对账跟上面sleep的区别。核心是在ThreadPoolExecutor.getTask这行上,通过研究过ThreadPoolExecutor的代码可以知道,是在捞队列里面的任务,这个时候park说明队列里面没有任务。很多时候我们线上排查问题遇到比较多的是这个状态,这里可以试一下让线程池繁忙起来的时候,线程池里线程的状态就全是Runnable,所以监控当前线程池的活跃线程数非常重要,它反映了我们系统压力的健康度。这里需要注意threadPoolExecutor.getPoolSize()和threadPoolExecutor.getActiveCount() 的区别,前者是线程池大小,后者是活跃线程数。
这些都是本地模拟的比较简单的情况,实际线上的情况会复杂很多,最终都跟线程池的机制密不可分,线程池的源码分析等有机会再整理下,同时上面没有分析NIO的情况,未完待续...