锁底层实现,乐观锁,悲观锁,底层AQS原理?
1 个回答
你会等一个人三五年吗?青鳞下面这分 4 块:**锁的家族与选型 → 乐观/悲观锁 → JVM 层 synchronized 的底层实现 → AQS(AbstractQueuedSynchronizer)工作原理**。 # 0. 锁的家族 & 选型速览(Java) * **悲观锁**(持有锁再干活,冲突时阻塞) * `synchronized`(内置锁,JVM 级) * `ReentrantLock`(可重入、可中断、公平/非公平、`Condition`) * `ReentrantReadWriteLock`(读写分离) * **乐观锁**(先干活,提交时用 CAS 校验) * `Atomic*`、`LongAdder`、`VarHandle`(JDK9+) * `StampedLock` 的 **optimistic read**(乐观读) * **并发工具(多数基于 AQS)** * `Semaphore`、`CountDownLatch`、`ReentrantLock/ReadWriteLock`、`StampedLock`(不基于 AQS)、`FutureTask`(内部也用 AQS) **选型口诀** * 读多写少、冲突低 → **乐观**(`Atomic`/`LongAdder`/`StampedLock` 乐观读) * 冲突高、需要阻塞/条件队列 → **悲观**(`ReentrantLock`/`synchronized`) * 需要读写并发 → `ReentrantReadWriteLock` 或 `StampedLock`(更快但 API 更容易踩坑) # 1. 乐观锁 vs 悲观锁(Java 语境) ### 悲观锁 * **思想**:先上锁再进入临界区,别人来就**阻塞**。 * **代表**:`synchronized`、`ReentrantLock`。 * **优点**:语义简单、不会忙等、可与条件队列配合。 * **缺点**:上下文切换/阻塞成本高;高并发下容易出现“锁队列”与**尾延迟**。 ### 乐观锁(CAS / Compare-And-Set) * **思想**:不加锁直接做,提交时用版本/期望值对比,失败就**重试**或回退。 * **代表**:`AtomicInteger.getAndIncrement()`、`VarHandle.compareAndSet`、`StampedLock.tryOptimisticRead`。 * **优点**:无阻塞、无内核调度,**低冲突**时极快。 * **缺点**:高冲突下**自旋退避**会浪费 CPU,可能饿死;存在 **ABA** 风险(`AtomicStampedReference`/`AtomicMarkableReference` 可缓解);**多字段原子性**需要额外设计。 **两段代码感性认识** * 悲观(阻塞,带条件队列): ```java ReentrantLock lock = new ReentrantLock(); Condition notEmpty = lock.newCondition(); Deque<Integer> q = new ArrayDeque<>(); void put(int x) { lock.lock(); try { q.addLast(x); notEmpty.signal(); } finally { lock.unlock(); } } int take() throws InterruptedException { lock.lock(); try { while (q.isEmpty()) notEmpty.await(); return q.removeFirst(); } finally { lock.unlock(); } } ``` * 乐观(CAS 自旋): ```java AtomicInteger ai = new AtomicInteger(0); int add10() { int v; do { v = ai.get(); } while (!ai.compareAndSet(v, v + 10)); return v + 10; } ``` * 乐观读(`StampedLock`): ```java StampedLock sl = new StampedLock(); double x, y; double distance() { long s = sl.tryOptimisticRead(); double lx = x, ly = y; if (!sl.validate(s)) { // 期间有写发生,降级为读锁 s = sl.readLock(); try { lx = x; ly = y; } finally { sl.unlockRead(s); } } return Math.hypot(lx, ly); } ``` # 2. `synchronized` 的底层实现(JVM 视角) > 关键点:**对象头(Mark Word) + Monitor(ObjectMonitor) + monitorenter/monitorexit 指令** > 内存语义:**进入/退出监视器**与 `volatile` 一样提供 **happens-before** 保证。 1. **字节码级** * `synchronized(obj) { ... }` 编译成 `monitorenter`/`monitorexit`。方法级 `synchronized` 编译为 ACC\_SYNCHRONIZED 标志,进入时隐式获取锁。 2. **对象头 / Mark Word**(JDK8 起常见) * 包含锁标志位、哈希、GC 信息等。 * 历史上有 **偏向锁**(JDK15 移除)→ 轻量级锁(栈上 Lock Record + CAS)→ 竞争激烈时**膨胀为重量级**(`ObjectMonitor`,内核阻塞/唤醒)。 3. **轻量级与膨胀**(简化流程) * **轻量级**:线程在栈上创建 Lock Record,用 **CAS** 把对象头的 Mark Word 指向该记录,**自旋**短暂等待;失败或竞争剧烈则**膨胀**。 * **重量级**:对象关联一个 `ObjectMonitor`,存在 **EntryList/WaitSet**;获取不到的线程通过 **`park()`** 阻塞,释放时 **`unpark()`** 唤醒。 4. **监视器与等待/通知** * `wait()/notify()/notifyAll()` 操作的是 **WaitSet**(与 `Condition` 类似,但属于对象监视器)。 5. **JIT 优化** * **锁消除**(逃逸分析判定无共享) * **锁粗化**(频繁短锁合并成大锁) * **自适应自旋**(在短期能拿到锁时先自旋,减少阻塞) # 3. AQS(AbstractQueuedSynchronizer)工作原理 > AQS 是**可复用的“队列式独占/共享同步器内核”**。大多数显式锁/同步器基于它实现(除了 `StampedLock`)。 **核心数据结构** * `volatile int state`:同步状态(如重入次数/许可证数),由子类定义语义。 * **FIFO 双向队列**(CLH 变体):`Node { prev, next, thread, waitStatus }` * `waitStatus`:`0`、`SIGNAL(-1)`、`CONDITION(-2)`、`CANCELLED(1)` 等 * **入队**:`enq()` 通过 **CAS 设置 tail**,多线程安全入队。 **获取流程(独占,`acquire`)** 1. 调用子类的 `tryAcquire(arg)`:如果成功,直接返回。 2. 失败则**构造 Node 入队**,然后在一个自旋里: * 看自己是不是 **head 的直接后继**,如果是则再尝试 `tryAcquire`; * 否则 **`LockSupport.park()` 挂起**;被唤醒后继续尝试。 > 这样**只有队首节点在“活跃尝试”**,避免全队自旋。 **释放流程(独占,`release`)** 1. 调用子类 `tryRelease(arg)` 成功 → **唤醒 head 的后继**(`unparkSuccessor`)。 2. 被唤醒的后继成为新的“活跃竞争者”。 **共享模式(`acquireShared/releaseShared`)** * 适用于 `Semaphore/CountDownLatch/ReadLock`,子类实现 `tryAcquireShared` 返回**剩余许可**(负值失败、非负成功),可以一次唤醒**多个**后继。 **公平 vs 非公平**(以 `ReentrantLock` 为例) * **非公平**:`lock()` 先 **CAS 抢占**,没成功再排队(吞吐更好,可能“插队”)。 * **公平**:检测队列是否有前驱,有则老老实实排队(更公平,吞吐略低)。 **可重入的实现(`ReentrantLock`)** * `state` 记录重入次数;同线程再次 `tryAcquire` 成功并 `state++`;释放时 `state--`,直至 0 才真正唤醒后继。 **`Condition` 的实现** * `await()`:当前线程从同步队列**转移到条件队列**(`waitStatus=CONDITION`),**释放锁**并 `park()`; * `signal()`:把条件队列头节点**转移回同步队列**,等待重新竞争锁;不是立刻拥有锁。 **一个最小的 AQS 自定义锁(演示)** ```java class MyMutex { private static class Sync extends AbstractQueuedSynchronizer { protected boolean isHeldExclusively() { return getState() == 1; } protected boolean tryAcquire(int acquires) { if (compareAndSetState(0, 1)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } protected boolean tryRelease(int releases) { if (getState() == 0) throw new IllegalMonitorStateException(); setExclusiveOwnerThread(null); setState(0); return true; } } private final Sync sync = new Sync(); public void lock() { sync.acquire(1); } public boolean tryLock() { return sync.tryAcquire(1); } public void unlock() { sync.release(1); } public boolean isLocked() { return sync.isHeldExclusively(); } } ``` > AQS 帮你处理**排队、阻塞/唤醒、公平性**;你只要定义“state 怎么加减”。 # 4. 常见坑 & 实战建议 * **`StampedLock` 乐观读**必须调用 `validate(stamp)` 校验;写多时退化严重,且**没有可重入/条件队列**,API 容易写错。 * **自旋 vs 阻塞**:冲突低 → 乐观/CAS + 轻量自旋;冲突高 → 直接阻塞(AQS/monitor),避免 CPU 风暴。 * **锁粒度**:拆分热点(锁分段/Striped)、读写分离、降低临界区工作量(IO/远程调用不要持锁)。 * **公平锁**:除非有明确诉求,否则用**非公平**获取更好的吞吐。 * **忘记释放**:`ReentrantLock` 必须 `try/finally`;`Condition.await()` 被中断/超时后也要正确处理。 * **内存语义**:`synchronized` 的进入/退出、`Lock` 的 `lock`/`unlock`、`volatile` 的写/读,都能建立 **happens-before**;不要混用导致误判。 * **LongAdder vs AtomicLong**:高并发计数优先 `LongAdder`(分段累加,减少热点 CAS 竞争)。 * **ABA 问题**:需要版本戳用 `AtomicStampedReference`/`AtomicMarkableReference`。 * **虚拟线程(JDK21+)**:阻塞型并发可以用 **虚拟线程**,很多场景下减少显式锁需求(但共享数据仍需同步)。 ## 面试“1 分钟总结说法” > Java 里悲观锁是 `synchronized`、`ReentrantLock` 等,先加锁、冲突时阻塞;乐观锁是用 CAS 做原子更新,如 `Atomic*` 和 `StampedLock` 的乐观读,低冲突时更快。`synchronized` 底层是对象头 Mark Word + 监视器,经历轻量级(CAS + 自旋)到膨胀为 `ObjectMonitor`(阻塞/唤醒),`monitorenter/exit` 提供 happens-before。大多数显式锁基于 **AQS**:维护一个 FIFO 等待队列,`state` 表示同步状态;获取失败就入队再 `park`,释放时唤醒后继;支持独占/共享、公平/非公平和 `Condition`。选型上,读多写少用乐观或读写锁;冲突高用 AQS 系列的阻塞锁;注意粒度与临界区,避免把 IO 放在锁里,并通过 LongAdder、锁分段、非公平策略等优化吞吐。发布于 2025-08-15 11:58 •IP属地 宁夏举报评论收藏
公众号进入网站体验更佳

