Arthas 之通过 thread 命令定位线程问题
通过 Arthas 中的 thread 命令,可以查看当前线程信息及线程的堆栈。从而可以定位线程问题。
thread 命令介绍
官方文档:
https://alibaba.github.io/arthas/thread.html
参数说明
使用 thread --help 获取 thread 命令的帮助信息:

主要参数说明如下:
| 参数名称 | 参数说明 | 
|---|---|
| id | 线程 id | 
| [n:] | 指定最忙的前 N 个线程并打印堆栈 | 
| [b] | 找出当前阻塞其他线程的线程 | 
| [i <value>] | 指定 cpu 占比统计的采样间隔,单位为毫秒 | 
线程常见状态
定位线程问题之前,先回顾一下线程的几种常见状态:
- RUNNABLE 运行中 
- TIMED_WAITIN 调用了以下方法的线程会进入 TIMED_WAITING: - Thread#sleep()
- Object#wait () 并加了超时参数
- Thread#join () 并加了超时参数
- LockSupport#parkNanos()
- LockSupport#parkUntil()
 
- WAITING 当线程调用以下方法时会进入 WAITING 状态: - Object#wait () 而且不加超时参数
- Thread#join () 而且不加超时参数
- LockSupport#park()
 
- BLOCKED 阻塞,等待锁 
cpu 占比的统计
这里的 cpu 统计的是,一段采样间隔内,当前 JVM 里各个线程所占用的 cpu 时间占总 cpu 时间的百分比。
其计算方法为: 首先进行一次采样,获得所有线程的 cpu 的使用时间 (调用的是
java.lang.management.ThreadMXBean#getThreadCpuTime这个接口),然后睡眠一段时间,默认 100ms,可以通过-i参数指定,然后再采样一次,最后得出这段时间内各个线程消耗的 cpu 时间情况,最后算出百分比。
注意: 这个统计也会产生一定的开销(JDK 这个接口本身开销比较大),因此会看到 as 的线程占用一定的百分比,为了降低统计自身的开销带来的影响,可以把采样间隔拉长一些,比如 5000 毫秒。
如果想看从 Java 进程启动开始到现在的 cpu 占比情况:可以使用 show-busy-java-threads 这个脚本
示例代码
首先编写一个有各种情况的测试类运行起来,再使用 Arthas 进行问题定位:
@Slf4j
public class ThreadDemo {
	private static HashSet<String> hashSet = new HashSet<String>();
	private static ExecutorService executorService = Executors.newFixedThreadPool(1);
	public static void main(String[] args) {
		
		addHashSetThread();	// 不断的向 hashSet 集合增加数据
		
		cpuHigh();		// 模拟 CPU 过高
		cpuNormal();
		
		thread();	// 模拟线程阻塞
		
		deadThread();	// 模拟线程死锁
	}
	/**
	 * 极度消耗CPU的线程
	 */
	private static void cpuHigh() {
		Thread thread = new Thread(() -> {
			while (true) {
				log.info("cpu start 100");
			}
		});
		// 添加到线程
		executorService.submit(thread);
	}
	/**
	 * 普通消耗CPU的线程
	 */
	private static void cpuNormal() {
		for (int i = 0; i < 10; i++) {
			new Thread(() -> {
				while (true) {
					log.info("cpu start");
					try {
						Thread.sleep(3000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}).start();
		}
	}
	/**
	 * 模拟线程阻塞,向已经满了的线程池提交线程
	 */
	private static void thread() {
		Thread thread = new Thread(() -> {
			while (true) {
				log.debug("thread start");
				try {
					Thread.sleep(3000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		});
		// 添加到线程
		executorService.submit(thread);
	}
	/**
	 * 死锁
	 */
	private static void deadThread() {
		/** 创建资源 */
		Object resourceA = new Object();
		Object resourceB = new Object();
		// 创建线程
		Thread threadA = new Thread(() -> {
			synchronized (resourceA) {
				log.info(Thread.currentThread() + " get ResourceA");
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				log.info(Thread.currentThread() + "waiting get resourceB");
				synchronized (resourceB) {
					log.info(Thread.currentThread() + " get resourceB");
				}
			}
		});
		Thread threadB = new Thread(() -> {
			synchronized (resourceB) {
				log.info(Thread.currentThread() + " get ResourceB");
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				log.info(Thread.currentThread() + "waiting get resourceA");
				synchronized (resourceA) {
					log.info(Thread.currentThread() + " get resourceA");
				}
			}
		});
		threadA.start();
		threadB.start();
	}
	/**
	 * 不断的向 hashSet 集合添加数据
	 */
	public static void addHashSetThread() {
		// 初始化常量
		new Thread(() -> {
			int count = 0;
			while (true) {
				try {
					hashSet.add("count" + count);
					Thread.sleep(10000);
					count++;
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}).start();
	}
}定位 CPU 使用较高的线程
上面的代码例子有一个 CPU 空转的死循环,非常的消耗 CPU性能,那么怎么找出来呢?
使用 thread 查看所有线程信息,同时会列出每个线程的 CPU 使用率,可以看到图里 ID 为 12 的线程 CPU 使用 100%。

使用命令 thread 12 查看 CPU 消耗较高的 12 号线程信息,可以看到 CPU 使用较高的方法和行数。

如果只是为了寻找 CPU 使用较高的线程,可以直接使用命令 thread -n [显示的线程个数] ,就可以排列出 CPU 使用率 Top N 的线程。

定位到的 CPU 使用最高的方法:

定位线程阻塞
上面的模拟代码里,定义了线程池大小为 1 的线程池,然后在 cpuHigh 方法里提交了一个线程,在 thread 方法再次提交了一个线程,后面的这个线程因为线程池已满,会阻塞下来。
使用 thread | grep pool 命令查看线程池里线程信息。

可以看到线程池有 WAITING 的线程:

定位线程死锁
上面的模拟代码里 deadThread 方法实现了一个死锁,使用 thread -b 命令查看直接定位到死锁信息。
