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

发布时间:2026/7/15 23:10:33
从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种状态NEW新建未启动RUNNABLE可运行包括正在运行和就绪BLOCKED等待获取锁WAITING无限期等待TIMED_WAITING有限期等待TERMINATED终止重点看与锁相关的状态转换graph LR BLOCKED--|获取锁|RUNNABLE RUNNABLE--|wait()|WAITING WAITING--|notify()|BLOCKED RUNNABLE--|sleep()|TIMED_WAITING TIMED_WAITING--|时间到|RUNNABLE2.2 状态转换的典型场景场景1锁竞争引发的BLOCKED// 线程1 synchronized(lock) { Thread.sleep(10000); // 持有锁睡眠 } // 线程2 synchronized(lock) { // 这里会阻塞 System.out.println(获取到锁); }用jstack查看时线程2会显示Thread-2 #12 prio5 blocked - waiting to lock 0x0000000712345678场景2wait/notify流程// 等待线程 synchronized(lock) { lock.wait(); // 释放锁进入WAITING // 被唤醒后需要重新获取锁 } // 唤醒线程 synchronized(lock) { lock.notifyAll(); }这里有个关键点被notify唤醒的线程会从WAITING变为BLOCKED必须再次获取锁才能继续执行。3. 实战问题排查指南3.1 使用jstack分析线程阻塞去年我们系统出现过一次性能骤降通过以下步骤定位问题获取线程dumpjstack -l pid thread_dump.txt分析关键信息DB-Connection-Thread-5 #25 daemon prio5 WAITING at java.lang.Object.wait(Native Method) - waiting on 0x000000076bf45678 at com.mysql.jdbc.ConnectionImpl.waitForLock HTTP-Worker-Thread-3 #33 prio5 BLOCKED - waiting to lock 0x000000076bf45678结论数据库连接线程持锁时间过长导致HTTP工作线程阻塞3.2 使用VisualVM监控对于图形化工具我推荐JDK自带的VisualVM打开jvisualvm选择目标Java进程查看线程选项卡右键点击线程可以执行线程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 prio5 waiting at sun.misc.Unsafe.park(Native Method)4.2 选择建议根据我的经验简单场景用synchronized代码更简洁需要超时或中断特性时用Lock高并发竞争激烈时用Lock性能更好曾经在秒杀系统中将synchronized改为ReentrantLock后TPS提升了近40%。但要注意正确使用try-finally释放锁。5. 最佳实践与避坑指南永远不要在循环外调用wait()经典写法synchronized(lock) { while(!condition) { lock.wait(); } }区分sleep和wait的唤醒方式sleep只能等时间到或被interruptwait可以被notify/notifyAll唤醒锁粒度控制我遇到过的一个坑在HashMap的全局锁里执行耗时IO操作应该拆分为细粒度锁 无锁IO操作状态监控建议生产环境建议定期采集线程dump关键指标BLOCKED线程数、WAITING超时情况记得有次凌晨处理告警发现大量TIMED_WAITING线程原来是某定时任务没有设置超时时间。这个经历让我养成了对所有阻塞操作都设置超时的习惯。