一、线程状态那些事儿
咱们先聊聊JVM线程的几种基本状态,就像人的不同状态一样:NEW(刚创建)、RUNNABLE(可运行)、BLOCKED(阻塞)、WAITING(等待)、TIMED_WAITING(限时等待)和TERMINATED(终止)。当线程卡住时,多半是停在BLOCKED或WAITING状态。比如下面这个Java示例:
public class DeadlockDemo {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lockA) {
System.out.println("Thread1持有lockA");
try { Thread.sleep(100); } catch (Exception e) {}
synchronized (lockB) { // 这里会卡住,因为lockB被thread2占着
System.out.println("Thread1拿到lockB");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lockB) {
System.out.println("Thread2持有lockB");
synchronized (lockA) { // 这里也会卡住,因为lockA被thread1占着
System.out.println("Thread2拿到lockA");
}
}
});
thread1.start();
thread2.start();
}
}
运行这段代码,你会发现两个线程互相等待对方释放锁,这就是典型的死锁。通过jstack工具可以轻松捕获这种状态。
二、诊断工具三板斧
1. jstack:线程快照分析
用jstack <pid>命令抓取线程栈,搜索BLOCKED或deadlock关键词。比如下面这段输出就明确指出了死锁:
Found one Java-level deadlock:
=============================
Thread-1:
waiting to lock monitor 0x00007f0134003e58 (object 0x000000076ab270c0, a java.lang.Object),
which is held by Thread-0
2. VisualVM:图形化利器
安装VisualVM插件后,直接看到线程时间线。红色区块表示阻塞状态,鼠标悬停还能看到阻塞的堆栈信息。
3. Arthas:在线诊断神器
阿里开源的Arthas可以动态观察线程状态。比如用thread -b命令直接找出阻塞线程:
[arthas@12345]$ thread -b
"Thread-1" Id=25 BLOCKED on java.lang.Object@6d5380c2 owned by "Thread-0" Id=24
三、实战:数据库连接池死锁
来看个更真实的场景。假设我们用HikariCP连接池时配置不当:
@Configuration
public class DatabaseConfig {
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setMaximumPoolSize(5); // 连接池大小设置过小
config.setConnectionTimeout(3000); // 超时时间太短
return new HikariDataSource(config);
}
}
// 业务代码中这样调用
@Transactional // Spring事务注解
public void transferMoney() {
jdbcTemplate.update("UPDATE accounts SET balance = ? WHERE id = ?", 100, 1);
otherService.process(); // 这里又调用了另一个事务方法
}
当并发请求量超过连接池大小时,线程会卡在获取数据库连接上,形成连锁阻塞。解决方案很简单:适当调大连接池大小,或者用@Async异步处理非关键路径操作。
四、防患于未然的技巧
- 锁排序:总是按固定顺序获取多个锁,避免交叉锁
- 超时机制:用
tryLock代替synchronized,比如:if (lock.tryLock(1, TimeUnit.SECONDS)) { try { /* 操作共享资源 */ } finally { lock.unlock(); } } - 避免同步方法调用:在同步方法内不要调用其他同步方法
- 线程转储定时任务:通过cron定期执行
jstack保存日志
五、高阶玩法:JVM内部锁优化
现代JVM其实很聪明,它会自动做这些优化:
- 锁消除:如果发现不可能存在竞争(比如局部对象),直接去掉锁
- 锁粗化:连续多个同步块合并成一个大块
- 偏向锁/自旋锁:减少真正的线程阻塞
但注意:这些优化在极端情况下可能失效,比如下面这段代码就逃不过阻塞:
public class ContendedDemo {
@sun.misc.Contended // 解决伪共享问题
static class Counter {
volatile long value;
}
public static void main(String[] args) {
Counter c = new Counter();
IntStream.range(0, 4).parallel().forEach(i -> {
for (int j = 0; j < 10_000_000; j++) {
synchronized (c) { // 这里仍然会引发竞争
c.value++;
}
}
});
}
}
总结
诊断线程问题就像破案,需要:
- 现场保护(保存线程转储)
- 线索分析(看阻塞堆栈)
- 重现现场(用相同参数复现)
- 改进方案(调整锁策略/超时时间)
记住:不是所有阻塞都是问题,但长时间的BLOCKED状态绝对是红色警报!
评论