从sleep与wait的锁释放差异,深入剖析Java线程状态转换

1. sleep与wait的核心差异解析

多线程编程中,sleep()wait()是最容易混淆的两个方法。刚入行时我也经常搞混,直到有次线上服务出现线程阻塞,用jstack分析dump文件时才真正理解它们的区别。先说结论:sleep是抱着锁睡觉,wait会先交出锁再等待

1.1 锁释放行为的本质区别

先看这段代码示例:

Object lock = new Object(); // 线程A执行 synchronized (lock) { System.out.println("线程A拿到锁"); Thread.sleep(3000); // 睡眠期间不释放锁 System.out.println("线程A释放锁"); } // 线程B执行 synchronized (lock) { System.out.println("线程B拿到锁"); }

运行时会发现:线程B必须等线程A完全执行完sleep的3秒后才能获取锁。这说明sleep()虽然让线程暂停执行,但不会释放已持有的锁

而换成wait()的版本:

Object lock = new Object(); // 线程A synchronized (lock) { System.out.println("线程A拿到锁"); lock.wait(3000); // 等待期间释放锁 System.out.println("线程A重新获得锁"); } // 线程B synchronized (lock) { System.out.println("线程B拿到锁"); }

这时线程B会立即获得锁并执行,3秒后线程A才会继续。这就是wait()的关键特性:主动释放锁并进入等待

1.2 状态转换的底层原理

通过jstack观察线程状态会更清晰:

  • sleep()会使线程进入TIMED_WAITING (sleeping)状态
  • wait()会使线程进入WAITING (on object monitor)TIMED_WAITING (on object monitor)状态

这两种状态的根本区别在于:

  • sleep时的线程仍然持有monitor锁(通过synchronized获取)
  • wait时的线程会将monitor锁归还给对象,这也是为什么wait必须在同步块中调用

我曾经在排查一个数据库连接池问题时,发现所有工作线程都卡在WAITING状态。最终发现是因为某个事务处理超时后没有正确调用notify,导致线程永久等待。这个案例充分说明了理解状态转换的重要性。

2. 线程状态转换全景剖析

2.1 Java线程的6种状态

根据Thread.State枚举,Java线程共有6种状态:

  1. NEW:新建未启动
  2. RUNNABLE:可运行(包括正在运行和就绪)
  3. BLOCKED:等待获取锁
  4. WAITING:无限期等待
  5. TIMED_WAITING:有限期等待
  6. TERMINATED:终止

重点看与锁相关的状态转换:

graph LR BLOCKED-->|获取锁|RUNNABLE RUNNABLE-->|wait()|WAITING WAITING-->|notify()|BLOCKED RUNNABLE-->|sleep()|TIMED_WAITING TIMED_WAITING-->|时间到|RUNNABLE

2.2 状态转换的典型场景

场景1:锁竞争引发的BLOCKED

// 线程1 synchronized(lock) { Thread.sleep(10000); // 持有锁睡眠 } // 线程2 synchronized(lock) { // 这里会阻塞 System.out.println("获取到锁"); }

用jstack查看时,线程2会显示:

"Thread-2" #12 prio=5 blocked - waiting to lock <0x0000000712345678>

场景2:wait/notify流程

// 等待线程 synchronized(lock) { lock.wait(); // 释放锁进入WAITING // 被唤醒后需要重新获取锁 } // 唤醒线程 synchronized(lock) { lock.notifyAll(); }

这里有个关键点:被notify唤醒的线程会从WAITING变为BLOCKED,必须再次获取锁才能继续执行。

3. 实战问题排查指南

3.1 使用jstack分析线程阻塞

去年我们系统出现过一次性能骤降,通过以下步骤定位问题:

  1. 获取线程dump
jstack -l <pid> > thread_dump.txt
  1. 分析关键信息
"DB-Connection-Thread-5" #25 daemon prio=5 WAITING at java.lang.Object.wait(Native Method) - waiting on <0x000000076bf45678> at com.mysql.jdbc.ConnectionImpl.waitForLock "HTTP-Worker-Thread-3" #33 prio=5 BLOCKED - waiting to lock <0x000000076bf45678>
  1. 结论:数据库连接线程持锁时间过长,导致HTTP工作线程阻塞

3.2 使用VisualVM监控

对于图形化工具,我推荐JDK自带的VisualVM:

  1. 打开jvisualvm
  2. 选择目标Java进程
  3. 查看"线程"选项卡
  4. 右键点击线程可以执行线程dump

特别有用的功能是可以看到:

  • 线程状态随时间的变化
  • 持有锁和等待锁的关系图
  • 死锁自动检测

4. 高级话题:Lock与synchronized的区别

4.1 状态表现的差异

同样的锁竞争场景,使用ReentrantLock时线程状态与synchronized不同:

Lock lock = new ReentrantLock(); // 线程1 lock.lock(); try { Thread.sleep(10000); } finally { lock.unlock(); } // 线程2 lock.lock(); // 这里会进入WAITING而不是BLOCKED try { System.out.println("获取到锁"); } finally { lock.unlock(); }

这是因为AQS的实现机制不同,jstack会显示:

"Thread-2" #12 prio=5 waiting at sun.misc.Unsafe.park(Native Method)

4.2 选择建议

根据我的经验:

  • 简单场景用synchronized(代码更简洁)
  • 需要超时或中断特性时用Lock
  • 高并发竞争激烈时用Lock(性能更好)

曾经在秒杀系统中,将synchronized改为ReentrantLock后,TPS提升了近40%。但要注意正确使用try-finally释放锁。

5. 最佳实践与避坑指南

  1. 永远不要在循环外调用wait()

    • 经典写法:
    synchronized(lock) { while(!condition) { lock.wait(); } }
  2. 区分sleep和wait的唤醒方式

    • sleep只能等时间到或被interrupt
    • wait可以被notify/notifyAll唤醒
  3. 锁粒度控制

    • 我遇到过的一个坑:在HashMap的全局锁里执行耗时IO操作
    • 应该拆分为:细粒度锁 + 无锁IO操作
  4. 状态监控建议

    • 生产环境建议定期采集线程dump
    • 关键指标:BLOCKED线程数、WAITING超时情况

记得有次凌晨处理告警,发现大量TIMED_WAITING线程,原来是某定时任务没有设置超时时间。这个经历让我养成了对所有阻塞操作都设置超时的习惯。