Java 并发编程笔记
写在前面的话
并发编程最早的实践都在操作系统里。
管程
理论和实践之间是有鸿沟的,要弥合这种鸿沟,通常需要我们去学习别人的实践。比如并发的标准设计思想来自于操作系统里的管程(monitor),我们应当学习管程,进而了解标准的并发模型-管理共享变量和线程(并发任务)间通信的基本理论模型。
MESA 模型
JAVA 采用 MESA 模型:
- 互斥(Mutual Exclusion):通过锁机制保证同一时刻只有一个线程能进入管程内部执行。
- 同步(Synchronization):利用条件变量(Condition Variable)实现线程间的等待与唤醒。
- Signal and Continue:
当线程发出通知(signal/notify)时,它继续持有锁并运行,而被唤醒的线程仅仅是进入就绪队列,并不立即抢占 CPU。
- 必须使用 while 循环:
由于线程被唤醒后不一定立即执行,当它重新获得锁时,环境条件可能已发生变化,因此必须在一个 while 循环中重新检查等待条件(while
(condition) { wait(); })。
为什么用 set 而不用 queue
- Queue 暗示 FIFO(先进先出):
- 如果我们叫它 EntryQueue,开发者会本能地认为:先来的线程一定先拿到锁。
- 但实际上,Java 的 synchronized 是非公平锁(Non-fair Lock)。
- 实际上更像“一堆人”而不是“一队人”:
- 在 JVM 的具体实现策略中,当锁被释放时,并不保证 EntrySet 中排在最前面的线程一定能抢到锁(可能被刚来的线程抢走,或者被随机唤醒)。
- 对于 WaitSet,notify() 唤醒的线程也不一定是先 wait() 的那个线程(取决于具体 JVM 实现)。
- 所以,用 Set(集合) 这个词能更准确地表达“这里有一群线程在等,但谁先出去不一定”的语义。
总结:叫 Set 是为了告诉你,不要依赖它们的唤醒顺序。
模型映射
| Mesa 模型 | Mesa 语义 | synchronized |
ReentrantLock |
Java State | 超时 | JVisualVM | 底层机制 |
|---|---|---|---|---|---|---|---|
| Entry Set (锁竞争) |
等待获取锁 | Monitor Entry List | AQS Sync Queue | BLOCKEDWAITING (parking) |
否 | Monitor Park |
ObjectMonitor LockSupport.park() |
| Wait Set (条件等待) |
等待条件满足 | Monitor Wait Set | AQS Condition Queue | WAITINGTIMED_WAITING |
是 | Wait Park |
ObjectMonitor LockSupport.park() |
| Owner (持有者) |
持有锁的线程 | Monitor Owner | exclusiveOwnerThread | RUNNABLE |
- | Running | - |
1. Mesa 模型的 “Signal and Continue” 语义
notify()/signal()后,通知者继续持有锁- 被唤醒的线程从 Wait Set 移入 Entry Set,必须重新竞争锁
- 唤醒路径:
synchronized: Wait Set → Entry Set (BLOCKED) → OwnerReentrantLock: Condition Queue → Sync Queue (WAITING) → Owner
2. Entry Set(锁竞争)❌ 永远不会有 TIMED_WAITING
synchronized不支持超时ReentrantLock.lock()不支持超时tryLock(timeout)不进队列,在当前线程自旋
3. Wait Set(条件等待)✅ 支持 TIMED_WAITING
wait(timeout)/await(timeout, unit)支持超时- 设计哲学:条件等待是主动等待业务条件,需要"等不到就放弃"的语义
4. JVisualVM 分类逻辑
- Monitor:
synchronized的BLOCKED状态 - Park:
LockSupport.park()导致的WAITING状态 - Wait:
Object.wait()导致的WAITING状态
5. 线程状态与 OS 调度
- RUNNABLE = OS Ready + OS Running(JVM 无法区分)
- BLOCKED / WAITING: 线程在 JVM 队列中,未持有 CPU
- Ready Queue 是 OS 层面的,JVM 不可见
Java 线程状态

1 | |
1 | |
线程状态列举
NEW
没有启动过的线程。
RUNNABLE
- 正在执行的线程。
- 可以被执行但没有拿到处理器资源。
BLOCKED
blocked 其实是 blocked waiting。
- 等待 monitor,进入 synchronized method/block
- 或者等 wait()/await()以后再次进入 synchronized method/block。解除 wait 以后以后不是直接 runnable,而是进入 blocked,如果 notify 后通知线程立刻离开同步块,则几乎不可能用程序观察到从 blocked 进入 runnable。如果通知者在 notify() 之后赖着不走(比如执行了一个耗时操作),或者同时有 100 个线程在竞争这把锁:
- 那个被唤醒的线程会长时间停留在 BLOCKED 状态,直到它抢到锁为止。可以通过 jstack 或者 Thread.getState() 清晰地观察到它处于 BLOCKED 状态。
WAITING
在调用这三种不计时方法以后,线程进入 waiting 态:
- Object.wait
- Thread.join
- LockSupport.park 我们经常在文档里看到的 thread lies dormant 就是被这个方法处理过的结果
waiting 意味着一个线程在等待另一个线程做出某种 action。wait 在等其他对象 notify 和 notifyAll,join 在等其他线程终结。
如:
java.util.concurrent.LinkedBlockingQueue.take -> java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await -> java.util.concurrent.locks.LockSupport.park
Reentrantlock 的 lock 接口的栈帧则是:
1 | |
jstack 总会告诉我们 waiting 的位置,比如等待某个 Condition 的 await 操作。
1 | |
对这个程序进行 thread dump,可以看出 ReentrantLock 就是依赖于 park 导致的 waiting:


如果使用 synchronized,则会显示 object monitor:

所以 waiting 可能是在条件变量上等待,也可能是在 synchronizer 本身上等待,不可一概而论。
按照 jvisualvm 的分类方法,线程还可以分为:
- 等待
- 驻留(park)
- 监视(monitor)
TIMED_WAITING
调用了计时方法,等待时间结束后才或者被其他唤醒方法唤醒结束等待。
- Thread.sleep
- Object.wait
- Thread.join
- LockSupport.parkNanos
- LockSupport.parkUntil
如:
java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take -> java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos -> java.util.concurrent.locks.LockSupport.parkNanos -> sun.misc.Unsafe.park
除了 sleep 以外,jstack 总会告诉我们 time_waiting 的位置,比如等待某个 Condition 的 await 操作。
TERMINATED
终结的线程,执行已经结束了。
中断退出也是一种结束。
几种线程状态的对比
- blocked:线程想要获取锁进入临界区之前,会求锁,求不到锁会进入 entry_set,然后放弃 cpu。高并发时 blocked 会增多。
- 工作线程池开始伸缩,扩容的时候:jvm.thread.waiting.count 的数量会变少。过程是,core 线程先满,然后队列再满,这时候等待从队列里获取任务,waiting 在 take 动作上的线程已经降为0了,然后开始产生非core线程,线程数才开始增长。
- 工作吞吐变多,而调用下游的工作线程在阻塞的时候,jvm.thread.time_waiting.count 会变多,因为 rpc 框架自带超时,而这些超时是会让工作线程进行计时等待的。
- 流量变大的时候,2 和 3 可能同时发生。
线程间方法的设计哲学
- 通常:
- 静态方法 = “我要操作当前线程”(self-operation)。static 相当于 per thread,一个好记的例子是通常 ThreadLocal 设置为 static 的,这样每个线程可以分到一个它的实例,而不是每个线程在每个对象里分到它的实例。
- 实例方法 = “我要操作指定线程”(cross-thread operation)。
- 这背后的逻辑是:
- 每个线程操作自己是比较安全的,static 可以默认在不指定对象的情况下操作自己。
- 而跨线程操作其他线程是比较危险的,因为其他线程的生死如果不是自然发展和结束的,很可能导致锁不释放,条件变量不正确设置,通知没有正确发出。这也就意味着系统可能死锁。
- 主动控制线程何时开始是安全的。
- 主动控制进程何时结束是危险的,因为你不能替他释放资源-这是禁止使用 stop、spend api 这类操作的全部理由。
- 可以跨线程操作的是比较温和的操作:
- start():可以让程序员开启线程周期。
- interrupt():可以设置一个标志位,算是轻微的主动写入别的线程状态的一种低侵入的 api。
- join(): 观测另一个对象的状态,通过内部自旋 wait 来等待另一个线程死亡。
- 其他 static 方法:
- yield():主动让出 CPU,让同优先级线程有机会运行。是对调度器的"建议",不保证效果。和 interrupt 的温和写入,但不必然强制操作形成对比。
特别的切换方法
LockSupport.park
也就是线程挂起。
condition 的 await 底层调用的是 LockSupport.park。这个方法的参数是一个用作 monitor 的对象,会被设置到 Object 的特定 Offset 上。
park 只能带来 waiting。所以 sync 和 conditionObject 其实都让 thread waiting ,只不过代表 thread 的 node 处在的队列不一样而已-线程 node 在 sync queue 和 condition queue 都是 waiting。
wait
这个方法是对 object 用的。
从 wait 中醒来会有伪唤醒的 case,所以醒来的时候一定要先检查唤醒条件是否已经得到满足。原理见《为什么条件锁会产生虚假唤醒现象(spurious wakeup)?》
join
1 | |
- 线程的 join 相当于当前线程在另一个会死亡的线程对象上等待,在 while 循环里无限 wait,在超时或者该线程死亡的时候从 wait 里解脱出来。
- 每个 thread 对象的内置状态变成死亡的时候,JVM 会主动调用这个对象的 notifyAll,这和任意条件对象的 wait 和 notifyAll 由程序员自己控制是不一样的。
关于 JMM、volatile、JUC、锁等内容,请参阅《线程安全与锁优化》。






