1. synchronized的可见性
加锁时:线程会清空工作内存中共享变量的值,从主内存重新加载最新值到工作内存。
解锁时:线程会将工作内存中修改后的共享变量值强制刷新到主内存。
Java 的happens-before原则明确规定:一个线程解锁监视器的操作,happens-before 于后续线程对同一个监视器的加锁操作。
晚上突然对happens-before原则有了新的认识,这是不是就是代码重排序时的if else,满足了这些规则,则不允许重排序
2. 加锁和解锁是怎么做的
JVM为每个锁对象关联一个monitor,monitorenter和monitorexit字节码指令
Monitor { int count; // 锁的重入计数器 Thread owner; // 当前持有锁的线程 Queue EntrySet; // 等待获取锁的线程队列 Queue WaitSet; // 调用wait()后等待被唤醒的线程队列 }
synchronized的加锁 / 解锁本质是对Monitor 的获取与释放,通过控制线程对 Monitor 的竞争,结合工作内存与主内存的同步机制,实现了线程安全的三大特性:原子性(互斥执行)、可见性(内存同步)、可重入性(计数器机制)。
3. 可重入性如何处理父子类
无论子类是否重写父类的synchronized方法,只要通过子类实例调用父类的synchronized方法,锁对象都是子类实例本身。
非静态synchronized方法的锁对象是this,而this永远指向 “当前实例”。
静态方法的锁对象是类本身。
4. EntrySet 和 WaitSet 的唤醒机制
EntrySet(获取锁的等待队列)
- 当持有锁的线程释放锁(计数器归 0)时,JVM 会唤醒EntrySet中阻塞的线程。
- 被唤醒的线程会立即进入锁竞争状态,谁能抢到锁取决于底层操作系统的线程调度机制(通常是抢占式,优先级高的线程可能更先获得调度)。
- 注意:唤醒操作不保证 “公平性”,即先进入队列的线程不一定先获得锁(非公平锁特性)。
WaitSet(调用 wait () 后的等待队列) - 线程调用wait()后会释放锁并进入WaitSet,需等待其他线程调用notify()/notifyAll()唤醒。
- notify()会随机唤醒WaitSet中的一个线程,notifyAll()会唤醒所有线程。
- 被唤醒的线程不会直接获得锁,而是先进入 EntrySet 队列,与其他线程一起重新竞争锁(同样是抢占式)。
5. 唤醒后的锁竞争
被唤醒的线程不会直接获得锁,而是进入就绪状态,参与锁的重新竞争:竞争的核心是修改 Monitor 的owner和count:谁能成功将owner设为自己并将count置 1,谁就获得锁。竞争结果由操作系统的线程调度器决定(抢占式),优先级高的线程可能更先获得调度,但无绝对保证。未抢到锁的线程会重新进入EntrySet,继续阻塞等待下一次唤醒。
synchronized的EntrySet唤醒逻辑是非公平的,体现在:
- 新线程可能插队:即使EntrySet中有等待的线程,新到达的线程仍可能直接抢到锁(无需进入队列)。
- 唤醒顺序不保证:EntrySet中的线程唤醒顺序与它们进入队列的顺序无关,先阻塞的线程可能后被唤醒。
6. 唤醒顺序不保证
EntrySet本质是一个无序的等待集合(而非严格的 FIFO 队列)
- 当线程阻塞时,会被添加到集合的任意位置(而非必须尾部)。
- 当唤醒时,JVM 会从集合中随机或按某种非顺序规则选择线程(如选择第一个找到的线程,或基于线程优先级筛选),而非按入队顺序选择。
为了最大化性能,避免为保证公平性而引入的额外开销 - 减少数据结构维护成本
- 避免 “唤醒 - 阻塞” 的恶性循环
- 与synchronized的非公平性设计一致
7. 非就绪态线程为什么不会被操作系统调度
操作系统通过线程状态标记区分是否可调度,阻塞态线程不在就绪队列中,自然不会被调度。
等待特定事件唤醒:阻塞态的线程会关联一个 “等待事件”(如锁释放),当事件发生时(如 JVM 触发唤醒),操作系统会将其从阻塞态转为就绪态,重新加入就绪队列,此时才可能被调度。
8. 非抢占式的缺点
非抢占式的 “致命缺陷”:无法解决 “线程饥饿” 与 “响应性” 问题
抢占式的 “补救措施”:通过 “锁优化” 减少无效切换
- JVM 层面:减少 “不必要的唤醒” 和 “阻塞”;轻量级锁
- 操作系统层面:通过 “调度策略” 优先调度 “更可能拿到锁的线程”;A持有锁,A优先级++,A释放锁,优先唤醒等待锁的B
抢占式调度模型—— 它不是 “完美无开销” 的,但却是 “在资源开销、响应性、公平性之间平衡的最优解”。