一、线程状态那些事儿

咱们先聊聊JVM线程的几种基本状态,就像人的不同状态一样:NEW(刚创建)、RUNNABLE(可运行)、BLOCKED(阻塞)、WAITING(等待)、TIMED_WAITING(限时等待)和TERMINATED(终止)。当线程卡住时,多半是停在BLOCKEDWAITING状态。比如下面这个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>命令抓取线程栈,搜索BLOCKEDdeadlock关键词。比如下面这段输出就明确指出了死锁:

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异步处理非关键路径操作。

四、防患于未然的技巧

  1. 锁排序:总是按固定顺序获取多个锁,避免交叉锁
  2. 超时机制:用tryLock代替synchronized,比如:
    if (lock.tryLock(1, TimeUnit.SECONDS)) {
        try { /* 操作共享资源 */ } 
        finally { lock.unlock(); }
    }
    
  3. 避免同步方法调用:在同步方法内不要调用其他同步方法
  4. 线程转储定时任务:通过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++;
                }
            }
        });
    }
}

总结

诊断线程问题就像破案,需要:

  1. 现场保护(保存线程转储)
  2. 线索分析(看阻塞堆栈)
  3. 重现现场(用相同参数复现)
  4. 改进方案(调整锁策略/超时时间)

记住:不是所有阻塞都是问题,但长时间的BLOCKED状态绝对是红色警报!