JUC——锁

张开发
2026/4/14 11:14:44 15 分钟阅读

分享文章

JUC——锁
1.volatile和synchronized有什么区别volatile关键字主要用于修饰变量确保该变量的更新操作对所有线程是可见的即一旦某个线程修改了volatile变量其他线程会立刻看到最新值。synchronized 关键字用于修饰方法或代码块确保同一时刻只有一个线程能够执行该方法或代码块从而实现互斥访问。volatile 靠「内存屏障」禁止指令重排从而保证有序性2.synchronized底层原理了解吗synchronized 的底层原理是每个对象都关联一个 Monitor监视器锁进入同步代码块时执行monitorenter尝试持有锁退出时执行monitorexit释放锁。Monitor 内部包含owner当前持有锁的线程EntryList竞争锁阻塞的线程队列WaitSet调用 wait () 等待的线程队列count: 加锁计数器重入次数当线程竞争激烈时会升级为重量级锁依赖操作系统互斥锁实现会引起线程阻塞和上下文切换。3.synchronized怎么保证可见性和volatile有什么不同synchronized 保证可见性依靠的是 JMM 的 happens-before 锁规则线程在解锁前会将工作内存的所有修改刷新到主内存下一个线程加锁时会使本地缓存失效从主内存重新读取从而保证可见性。它和 volatile 原理不同volatile 只针对单个变量靠内存屏障实现synchronized 针对整个同步块靠锁的先行发生规则实现并且额外保证原子性。4.synchronized如何保证有序性和volatile有什么不同synchronized 保证有序性是通过 线程互斥执行同步块结合 锁的 happens-before 规则即使临界区内指令可以重排对外表现依然有序。而 volatile 是通过 插入内存屏障直接禁止指令重排从硬件层面保证有序。区别在于synchronized 是 “结果有序”volatile 是 “执行过程禁止重排”synchronized 保证原子性volatile 不保证。synchronized 的可重入性是通过 Monitor 中的计数器实现的同一个线程再次获取锁时判断 owner 是自己就将计数器 1退出时 -1计数器为 0 时才真正释放锁避免了自我死锁。5.synchronized锁有几种状态锁升级的过程1. 无锁状态没有线程竞争对象头 MarkWord 里存的是哈希码、分代年龄2. 偏向锁最乐观适用场景只有一个线程反复获取锁几乎没有竞争。机制锁会偏向第一个获取它的线程线程下次再进来不需要 CAS不需要竞争直接判断对象头里的线程 ID 是不是自己是就直接执行无需额外加锁几乎无开销像没加锁一样。3. 轻量级锁自旋锁适用场景多个线程在不同的时间段交替使用锁竞争不激烈。触发当第二个线程来竞争偏向锁就会升级为轻量级锁机制线程在自己栈帧中创建锁记录用 CAS 尝试把对象头 MarkWord 指向自己的锁记录失败就自旋循环重试不阻塞优点不进入内核态避免上下文切换开销4. 重量级锁最悲观适用场景竞争激烈自旋很多次也拿不到锁。触发自旋超过一定次数JVM 控制或有线程排队直接升级为重量级锁机制依赖 ObjectMonitor 监视器锁线程阻塞进入内核态有 EntryList、WaitSet 等队列缺点重、开销大、有上下文切换综合synchronized 锁升级是 JDK1.6 为优化性能引入的机制随着竞争逐渐加剧锁会从无锁 → 偏向锁 → 轻量级锁 → 重量级锁逐步升级偏向锁适用于单线程重复获取锁轻量级锁通过 CAS 自旋减少阻塞重量级锁依赖操作系统 Monitor 实现互斥锁只会升级不会降级。锁的升级过程出自三分恶面渣逆袭synchronized 锁升级过程1. 初始状态无锁特征对象未被线程竞争MarkWord 处于未锁定状态。逻辑线程首次访问同步代码块进入锁状态判断。2. 第一阶段偏向锁最乐观触发条件只有一个线程持续获取锁几乎无竞争。核心操作线程通过 CAS 将对象头 MarkWord 中的 Thread ID 设为当前线程 ID。后续该线程再次获取锁只需比较 Thread ID无需额外开销。图中对应检查对象头是否为偏向锁是当前线程则直接执行同步代码。3. 第二阶段轻量级锁自旋锁触发条件出现第二个线程竞争锁偏向锁升级。核心操作线程在自己栈帧中建立 Lock Record。通过 CAS 尝试将对象头 MarkWord 指向自己的 Lock Record轻量级锁标记 00。竞争失败则进入 自旋循环重试不进入内核态。图中对应CAS 替换 MarkWord 失败 - 升级为轻量级锁 - 自旋尝试。4. 第三阶段重量级锁悲观锁触发条件轻量级锁自旋次数超过阈值JVM 控制。竞争激烈有线程排队。核心操作升级为重量级锁标记位 10依赖 ObjectMonitor。竞争失败的线程进入 EntryList 阻塞发生内核态切换开销大。图中对应CAS 多次失败 - 升级为重量级锁 - 线程阻塞。6.AQS了解吗CAS有啥用AQS 是 JUC 底层的抽象队列同步器内部维护一个 volatile int state 和一个 CLH 等待队列通过 CAS 实现锁的竞争与释放ReentrantLock、Semaphore、CountDownLatch 都基于 AQS。AQS 的思想是:通过一个 volatile state 表示同步状态利用 CAS 实现无锁抢占抢不到资源的线程进入 CLH 队列阻塞等待释放时精准唤醒后继续采用模板方法模式提供独占 / 共享两套机制实现一套高度通用的同步框架。核心三件套state 变量volatile int表示锁状态0 无锁1 已加锁1 重入双向链表队列存放抢锁失败的线程叫 CLH 队列CAS用来安全修改 state实现无锁竞争线程抢锁时执行unsafe.compareAndSwapInt(this, stateOffset, 0, 1);多个线程可以同时调用 CAS所以叫抢占而CPU 硬件保证只有一个能成功其他全部失败。CAS 即比较并交换是 CPU 级别的原子操作通过(内存值, 预期值, 新值)实现无锁更新。本质上是一种乐观锁用于比较一个变量的当前值是否等于预期值如果相等则更新值否则重试。在 CAS 中有三个值V要更新的变量(var)E预期值(expected)N新值(new)先判断 V 是否等于 E如果等于将 V 的值设置为 N如果不等说明已经有其它线程更新了 V当前线程就放弃更新。这个比较和替换的操作需要是原子的不可中断的。Java 中的 CAS 是由 Unsafe 类实现的。原子性是通过发出一个 LOCK 指令进行总线锁定直到指令执行完之后才会允许其他线程操作。优点轻量、无锁、高并发下性能好。缺点① ABA 问题值 A → B → ACAS 一看还是 A以为没变就替换了实际上已经被改过解决加版本号这样CAS操作的时候不仅比较变量值还比较版本号。② 长时间自旋消耗 CPUCAS 失败会循环重试高并发下 CPU 会飙高。解决加一个自旋次数的限制超过一定次数就切换到synchronized 挂起线程。③ 只能保证一个变量的原子性不能像锁一样同时保护多个变量。AQS 设计时就抽象了两种资源共享方式独占模式Exclusive同一时间只有一个线程持有→ ReentrantLock、ReentrantReadWriteLock.WriteLock共享模式Shared允许多个线程同时持有→ Semaphore、CountDownLatch、ReadLock7.ReentrantLock是怎么实现的ReentrantLock 是基于 AQS 实现的可重入独占锁通过 CAS 修改 state 抢锁失败时进入 AQS 队列阻塞释放时唤醒队列线程通过重写tryAcquire/tryRelease实现可重入与公平 / 非公平锁逻辑支持公平锁、非公平锁可以中断、超时等待。8.公平锁和非公平锁一、区别1. 公平锁FairSync严格按照CLH 队列顺序唤醒线程获取锁的顺序和线程请求锁的顺序相同。新线程来抢锁时先看队列里有没有人在等有 → 直接进队不抢没有 → 才去 CAS 抢锁优点不会有线程 “饥饿”每个线程早晚都能拿到缺点吞吐量低频繁唤醒、切换线程成本高2. 非公平锁NonfairSync默认新线程一来不管队列有没有人直接 CAS 抢锁抢到就直接执行抢不到再进队优点吞吐量高线程切换少性能好缺点可能产生线程饥饿队列里的线程一直抢不过新来的二、实现逻辑源码级区别ReentrantLock 内部两个 AQS 子类FairSync、NonfairSync1. 非公平锁 lock ()final void lock() { // 一上来就插队抢锁 if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }2. 公平锁 lock ()final void lock() { // 不抢直接走正常 acquire 流程 acquire(1); }三、核心差异在 tryAcquire非公平锁 tryAcquirefinal boolean nonfairTryAcquire(int acquires) { // 直接抢不看队列 if (compareAndSetState(0, 1)) { setExclusiveOwnerThread(current); return true; } // ... 重入逻辑 }公平锁 tryAcquirefinal void lock() { // 不抢直接走正常 acquire 流程 acquire(1); }关键一句公平锁在抢锁前会先判断hasQueuedPredecessors() 队列里有人我就不抢直接排队总结公平锁新线程会先检查 AQS 队列是否有等待线程有则排队遵循 FIFO不会饥饿但性能较低。非公平锁新线程直接 CAS 抢锁不排队抢到就执行吞吐量更高但可能造成线程饥饿。实现区别公平锁在tryAcquire前多一步hasQueuedPredecessors()判断队列是否为空。但是要注意非公平锁并非抢占正在运行线程的锁也不会终止持有锁的线程。它只是在锁释放的瞬间允许新到来的线程与队列中等待的线程同时竞争锁新线程有可能先抢到从而实现 “插队”提高吞吐量。9.线程死锁是什么产生死锁的条件如何解决死锁就是两个或多个线程互相持有对方需要的锁又都不释放自己的锁导致永久相互等待程序卡住不动。产生死锁的四个必要条件互斥条件锁是独占的同一时间只能一个线程持有。请求与保持线程已经持有一个锁又去请求别的锁且不释放已持有的锁。不可剥夺线程持有的锁只能自己释放别人不能强行抢走。循环等待多个线程形成一个环形的锁等待链解决方法思路就是破坏上面的四个互斥条件1最常用、最有效统一锁的获取顺序让所有线程都按相同顺序去拿锁都先拿锁 A再拿锁 B就不会形成循环等待。2定时锁 / 尝试锁用ReentrantLock.tryLock(timeout)拿不到锁就放弃释放已持有的锁破坏 “请求与保持”3减少锁的嵌套尽量不要一个锁里面再套另一个锁降低死锁概率。4粗锁替代细锁不用多把小锁改用一个大锁从根源避免循环等待。5检测与恢复很少用通过监控工具检测死锁然后强制终止某个线程。10.对自旋锁、乐观锁、悲观锁的理解自旋锁是指当线程尝试获取锁时如果锁已经被占用线程不会立即阻塞而是通过自旋也就是循环等待的方式不断尝试获取锁此时线程始终处于用户态不会挂起线程从而跳过了操作系统调度的繁琐过程减少线程上下文切换的开销。但是如果锁被占用时间过长会导致线程空转浪费 CPU 资源。在实际开发中应该设置自旋次数或者超时时间一旦超出阈值线程就会放弃锁或者进入阻塞状态。乐观锁认为冲突不会总是发生所以在操作前不加锁而是在更新数据时检查是否有其他线程修改了数据。如果发现数据被修改了就会重试。悲观锁认为每次访问共享资源时都会发生冲突所在在操作前一定要先加锁防止其他线程修改数据

更多文章