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 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分析线程阻塞
去年我们系统出现过一次性能骤降,通过以下步骤定位问题:
- 获取线程dump:
jstack -l <pid> > thread_dump.txt- 分析关键信息:
"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>- 结论:数据库连接线程持锁时间过长,导致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 prio=5 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只能等时间到或被interrupt
- wait可以被notify/notifyAll唤醒
锁粒度控制
- 我遇到过的一个坑:在HashMap的全局锁里执行耗时IO操作
- 应该拆分为:细粒度锁 + 无锁IO操作
状态监控建议
- 生产环境建议定期采集线程dump
- 关键指标:BLOCKED线程数、WAITING超时情况
记得有次凌晨处理告警,发现大量TIMED_WAITING线程,原来是某定时任务没有设置超时时间。这个经历让我养成了对所有阻塞操作都设置超时的习惯。