当前位置: 首页 > news >正文

【大白话说Java面试题 第107题】【并发篇】第7题:说说 Lock 锁?

📌微服务架构:基于Spring Cloud Alibaba的分布式事务处理:Seata AT模式与Sentinel协同实现高并发下数据最终一致性

第7题:说说 Lock 锁?

📚回答:

  • 核心考点
    Lock是 JUC 包的核心接口,大厂面试不会只问"有哪些方法",而是深入考察AQS(AbstractQueuedSynchronizer)框架原理ReentrantLock 的公平/非公平实现差异Condition 条件队列与 Object wait/notify 的本质区别读写锁的锁降级与锁升级策略,以及StampedLock 的乐观读模式。面试官真正想判断的是:你是否理解 Lock 体系从"API 层"到"AQS 框架层"再到"CAS + 队列层"的完整架构,以及能否在生产环境中正确选型。

1. Lock 接口体系与核心方法

java.util.concurrent.locks.Lock是 Java 显式锁的根接口,定义了与synchronized不同的锁范式 [citation:1]:

方法功能与 synchronized 对比
lock()阻塞获取锁,不可中断synchronized等价
lockInterruptibly()阻塞获取锁,可响应中断synchronized不支持
tryLock()非阻塞尝试获取,立即返回 booleansynchronized不支持
tryLock(time, unit)限时阻塞获取,超时返回 falsesynchronized不支持
unlock()释放锁(必须在 finally 中调用)synchronized 自动释放
newCondition()创建条件队列,支持多条件等待synchronized 只有一个 wait/notify

关键差异synchronized是"隐式锁"(JVM 自动管理),Lock是"显式锁"(开发者手动控制),提供了更精细的并发控制能力 [citation:1]。


2. ReentrantLock 的底层实现——AQS 框架
  • 2.1 AQS 核心架构
    ReentrantLock的底层依赖AbstractQueuedSynchronizer(AQS),这是 JUC 包中几乎所有同步组件的基石(ReentrantLock、CountDownLatch、Semaphore、ReentrantReadWriteLock 等)[citation:2]。

    AQS 核心结构 [citation:2][citation:3]:

    ┌─────────────────────────────────────────┐ │ AbstractQueuedSynchronizer │ ├─────────────────────────────────────────┤ │ volatile int state // 同步状态 │ │ volatile Node head // 队列头节点 │ │ volatile Node tail // 队列尾节点 │ │ Thread exclusiveOwnerThread // 独占锁持有者 │ └─────────────────────────────────────────┘

    state 字段的语义

    • ReentrantLock:0 表示无锁,>0 表示重入次数;
    • CountDownLatch:初始值为计数,减到 0 时唤醒等待线程;
    • Semaphore:剩余可用许可数。

    Node 节点结构

    ┌─────────────────────────────────────────┐ │ Node (双向链表) │ ├─────────────────────────────────────────┤ │ volatile int waitStatus // 节点状态 │ │ volatile Node prev // 前驱节点 │ │ volatile Node next // 后继节点 │ │ volatile Thread thread // 绑定的线程 │ │ Node nextWaiter // 条件队列链接 │ └─────────────────────────────────────────┘
    waitStatus含义
    CANCELLED1节点已取消(超时或中断)
    SIGNAL-1后继节点需要被唤醒
    CONDITION-2节点在条件队列中
    PROPAGATE-3共享模式下向后传播唤醒
  • 2.2 非公平锁的获取流程

    // ReentrantLock.NonfairSync.lock()finalvoidlock(){if(compareAndSetState(0,1))// ① 直接 CAS 尝试获取setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);// ② CAS 失败,进入 AQS 队列}

    acquire(1)的核心逻辑 [citation:2]:

    1. tryAcquire(1):再次尝试获取(检查 state 是否为 0,或是否重入);
    2. addWaiter(Node.EXCLUSIVE):创建 Node 节点,CAS 加入队列尾部;
    3. acquireQueued():自旋/CAS 检查前驱是否为头节点,是则尝试获取锁;
    4. park():自旋失败则调用LockSupport.park()挂起线程。

    非公平性体现:新线程到达时先 CAS 尝试(不排队),失败后才进入队列。这允许"插队",吞吐量更高但可能饥饿 [citation:3]。

  • 2.3 公平锁的获取流程

    // ReentrantLock.FairSync.lock()finalvoidlock(){acquire(1);// 直接走 acquire,没有前置 CAS}protectedfinalbooleantryAcquire(intacquires){finalThreadcurrent=Thread.currentThread();intc=getState();if(c==0){if(!hasQueuedPredecessors()&&// ← 关键:检查是否有前驱节点compareAndSetState(0,acquires)){setExclusiveOwnerThread(current);returntrue;}}// ... 重入逻辑}

    公平性体现hasQueuedPredecessors()检查队列中是否有等待线程,有则不允许插队,必须排队 [citation:3]。

  • 2.4 锁释放流程

    // ReentrantLock.unlock() → AQS.release(1)publicfinalbooleanrelease(intarg){if(tryRelease(arg)){// state 减 1,检查是否为 0Nodeh=head;if(h!=null&&h.waitStatus!=0)unparkSuccessor(h);// 唤醒头节点的后继returntrue;}returnfalse;}

    关键设计:释放锁时只唤醒一个后继线程(unparkSuccessor),被唤醒的线程继续自旋竞争锁。这是"独占模式"的设计 [citation:2]。


3. 公平锁 vs 非公平锁深度对比
对比维度非公平锁(默认)公平锁
获取策略新线程先 CAS 尝试,失败再排队必须检查队列,有等待者则排队
吞吐量高(减少线程切换)低(严格排队,切换频繁)
饥饿风险存在(新线程可能一直插队)无(严格 FIFO)
适用场景高并发、短临界区长临界区、需要避免饥饿
性能差距比公平锁高 10%~20%相对低

压测数据:在 100 线程竞争下,非公平锁的吞吐量约为公平锁的 1.2~1.5 倍 [citation:3]。


4. Condition 条件队列——多条件等待/唤醒
  • 4.1 与 Object wait/notify 的本质区别
    synchronized只有一个隐式条件队列(waitSet),而ReentrantLock可以创建多个Condition,实现更精细的线程协作 [citation:1]。

    特性Object wait/notifyCondition await/signal
    条件队列数量1 个(每个对象一个)多个(每个 Lock 可创建多个)
    唤醒精度notify()随机唤醒一个,notifyAll()全部唤醒signal()唤醒一个,signalAll()全部唤醒,可精确控制
    使用前提必须持有对象锁必须持有 Lock 锁
    中断响应不区分中断原因awaitUninterruptibly()可选
  • 4.2 Condition 的底层实现
    每个Condition维护一个独立的条件队列(单向链表),与 AQS 的主队列(同步队列)分离 [citation:2]:

    Lock (AQS) ├── 同步队列(Sync Queue):双向链表,等待锁的线程 │ head → Node(T1) ↔ Node(T2) ↔ Node(T3) → tail │ ├── Condition1 条件队列(Wait Queue):单向链表 │ firstWaiter → Node(T4) → Node(T5) → null │ └── Condition2 条件队列(Wait Queue):单向链表 firstWaiter → Node(T6) → null

    await() 流程[citation:2]:

    1. 释放 Lock(fullyRelease),保存重入次数;
    2. 创建 Node 加入 Condition 的条件队列;
    3. park()挂起线程;
    4. signal()唤醒后,从条件队列转移到同步队列,重新竞争 Lock。

    signal() 流程[citation:2]:

    1. 检查是否持有 Lock;
    2. 从条件队列头部取出一个 Node;
    3. 将其转移到同步队列尾部;
    4. 设置前驱节点状态为 SIGNAL,唤醒该线程。
  • 4.3 经典示例——生产者消费者(双条件)

    publicclassBoundedBuffer<T>{privatefinalLocklock=newReentrantLock();privatefinalConditionnotFull=lock.newCondition();// 队列不满条件privatefinalConditionnotEmpty=lock.newCondition();// 队列不空条件privatefinalQueue<T>queue=newLinkedList<>();privatefinalintcapacity;publicvoidput(Titem)throwsInterruptedException{lock.lock();try{while(queue.size()==capacity){notFull.await();// 队列满,等待"不满"信号}queue.add(item);notEmpty.signal();// 通知消费者"不空"了}finally{lock.unlock();}}publicTtake()throwsInterruptedException{lock.lock();try{while(queue.isEmpty()){notEmpty.await();// 队列空,等待"不空"信号}Titem=queue.poll();notFull.signal();// 通知生产者"不满"了returnitem;}finally{lock.unlock();}}}

    优势notFullnotEmpty是两个独立的条件队列,生产者只唤醒消费者,消费者只唤醒生产者,避免了notifyAll()的"惊群效应" [citation:1]。


5. ReentrantReadWriteLock——读写分离
  • 5.1 设计动机
    读操作不修改数据,多个读线程可以并行;写操作需要独占。ReentrantReadWriteLock将锁拆分为读锁(共享)和写锁(独占),提升读多写少场景的并发度 [citation:4]。

    锁类型获取条件并发性
    读锁readLock()无写锁或写锁由当前线程持有多个读线程可同时持有
    写锁writeLock()无读锁且无其他写锁独占
  • 5.2 AQS 中的实现
    AQS 的state被拆分为高 16 位(读锁计数)和低 16 位(写锁重入次数):

    state (32 bit) ├─ 高 16 bit:读锁持有数(包括重入) └─ 低 16 bit:写锁重入次数(0 表示无写锁)

    读锁获取:检查低 16 位是否为 0(无写锁),是则高 16 位 +1;
    写锁获取:检查高 16 位是否为 0(无读锁)且低 16 位为 0 或当前线程持有,是则低 16 位 +1 [citation:4]。

  • 5.3 锁降级(Lock Downgrading)

    ReentrantReadWriteLockrwl=newReentrantReadWriteLock();ReentrantReadWriteLock.ReadLockrl=rwl.readLock();ReentrantReadWriteLock.WriteLockwl=rwl.writeLock();wl.lock();try{// 修改数据data=newValue;// 锁降级:在释放写锁前获取读锁rl.lock();// ← 允许!当前线程持有写锁时可直接获取读锁}finally{wl.unlock();// 释放写锁,此时持有读锁}try{// 使用数据(保证看到最新值,同时允许其他读线程并行)use(data);}finally{rl.unlock();}

    作用:保证数据修改后的可见性,同时降低锁粒度,允许其他读线程并行 [citation:4]。

    ⚠️ 注意:不支持锁升级(持有读锁时获取写锁会导致死锁)。


6. StampedLock——乐观读与性能优化

JDK 8 引入的StampedLock,在读多写少场景下性能优于ReentrantReadWriteLock[citation:5]。

模式方法特点
写锁writeLock()独占,与读写锁类似
悲观读锁readLock()共享,与读写锁类似
乐观读tryOptimisticRead()无锁读取,返回 stamp,验证失败再转悲观读

乐观读示例[citation:5]:

publicclassPoint{privatedoublex,y;privatefinalStampedLocksl=newStampedLock();// 乐观读publicdoubledistanceFromOrigin(){longstamp=sl.tryOptimisticRead();// ① 获取乐观读 stamp(无锁)doublecurrentX=x,currentY=y;// ② 拷贝变量if(!sl.validate(stamp)){// ③ 验证 stamp 是否被写操作修改stamp=sl.readLock();// ④ 失败,转为悲观读锁try{currentX=x;currentY=y;}finally{sl.unlockRead(stamp);}}returnMath.sqrt(currentX*currentX+currentY*currentY);}}

原理:乐观读不真正获取锁,只是记录一个版本戳(stamp)。读取完成后验证 stamp,如果期间没有写操作,直接返回结果;如果有写操作,升级为悲观读锁重试 [citation:5]。

性能对比:在纯读场景下,StampedLock 的乐观读性能接近无锁,远超 ReentrantReadWriteLock。


7. Lock 与 synchronized 的选型决策
场景推荐方案理由
简单同步代码块synchronized语法简洁,JVM 自动优化(偏向锁、轻量级锁)
需要超时获取ReentrantLock.tryLock(time, unit)避免无限等待
需要响应中断ReentrantLock.lockInterruptibly()可中断阻塞
需要公平锁ReentrantLock(true)避免饥饿
需要多条件等待ReentrantLock + Condition精确唤醒,避免惊群
读多写少ReentrantReadWriteLock读锁共享,提升并发
读极多写极少StampedLock(乐观读)无锁读,性能最高
高并发计数LongAdder分段累加,无锁

8. 生产环境避坑指南
  • 8.1 必须在 finally 中 unlock

    // ❌ 错误:异常时锁不释放lock.lock();doSomething();// 如果抛异常,锁永远不释放lock.unlock();// ✅ 正确lock.lock();try{doSomething();}finally{lock.unlock();// 确保释放}
  • 8.2 避免锁顺序导致的死锁

    // ❌ 错误:不同顺序获取锁// 线程 A:lock1.lock(); lock2.lock();// 线程 B:lock2.lock(); lock1.lock();// ✅ 正确:全局统一顺序// 所有线程都按 lock1 → lock2 的顺序获取
  • 8.3 注意 tryLock 的返回值

    // ❌ 错误:忽略返回值lock.tryLock();doSomething();// 如果没获取到锁,也在执行!// ✅ 正确if(lock.tryLock()){try{doSomething();}finally{lock.unlock();}}
  • 8.4 StampedLock 不支持重入
    StampedLock不是可重入锁,同一线程重复获取会导致死锁 [citation:5]。

  • 8.5 读写锁的写锁饥饿
    ReentrantReadWriteLock默认非公平模式下,读线程可能持续涌入,导致写线程长期等待(写饥饿)。可通过ReentrantReadWriteLock(true)使用公平模式,或限制读锁持有时间 [citation:4]。


9. 面试官追问与高分回答模板
  • 追问 1:“说说 Lock 锁?”

    低分回答:“Lock 是显式锁,需要手动 lock/unlock,支持公平锁、可中断、超时。”(太浅)

    高分回答

    "Lock是 JUC 包的显式锁接口,与synchronized相比提供了更精细的并发控制能力:

    1. 可中断lockInterruptibly()允许在等待锁时响应中断;
    2. 超时获取tryLock(time, unit)避免无限等待;
    3. 公平性:可选公平/非公平模式;
    4. 多条件队列newCondition()创建多个条件队列,精确唤醒。
      底层实现上,ReentrantLock依赖 AQS 框架,通过 CAS + 自旋 + 队列实现锁的获取与释放。AQS 的state字段表示同步状态,Node双向链表维护等待队列。
      选型上,简单场景用synchronized,需要超时/中断/多条件时用ReentrantLock,读多写少用ReentrantReadWriteLock,读极多用StampedLock。" [citation:1][citation:2]
  • 追问 2:“ReentrantLock 的公平锁和非公平锁底层怎么实现的?”

    高分回答

    "两者都基于 AQS 框架,差异在tryAcquire方法:

    • 非公平锁(默认):新线程到达时先 CAS 尝试获取锁compareAndSetState(0, 1)),成功则直接获取,失败才进入 AQS 队列。这允许’插队’,吞吐量高但可能饥饿。
    • 公平锁tryAcquire中先调用hasQueuedPredecessors()检查队列中是否有等待线程。如果有,即使state为 0 也不允许获取,必须排队。
      非公平锁性能更高(减少线程切换),但公平锁避免饥饿。压测显示非公平锁吞吐量比公平锁高 10%~20%。" [citation:2][citation:3]
  • 追问 3:“Condition 和 Object 的 wait/notify 有什么区别?”

    高分回答

    "核心区别有三点:

    1. 队列数量synchronized每个对象只有一个隐式 waitSet;ReentrantLock可以创建多个Condition,每个有独立的条件队列。
    2. 唤醒精度notify()随机唤醒一个,notifyAll()全部唤醒;signal()唤醒一个,signalAll()全部唤醒,且可以精确控制唤醒哪个条件的线程。
    3. 底层实现wait()将线程加入对象头的 Monitor 的_WaitSetawait()将线程加入Condition条件队列(与 AQS 主队列分离),被signal()后转移到 AQS 主队列重新竞争锁。
      经典应用是生产者消费者模型:用notFullnotEmpty两个 Condition,生产者只唤醒消费者,消费者只唤醒生产者,避免notifyAll()的惊群效应。" [citation:1][citation:2]
  • 追问 4:“AQS 是什么?它的核心设计是什么?”

    高分回答

    "AQS(AbstractQueuedSynchronizer)是 JUC 包的并发框架基石,ReentrantLockCountDownLatchSemaphoreReentrantReadWriteLock都基于它实现。
    核心设计是‘状态 + 队列’模式:

    1. state 字段volatile int表示同步状态,具体语义由子类定义(ReentrantLock 的重入次数、CountDownLatch 的剩余计数等);
    2. FIFO 队列Node双向链表维护等待线程,头节点是持有锁的线程(或虚拟节点),后续节点自旋/CAS 检查前驱;
    3. 模板方法模式:AQS 定义了acquire/release框架,子类只需实现tryAcquire/tryRelease等钩子方法。
      获取锁时:CAS state → 失败则加入队列尾部 → 自旋检查前驱 → 失败则LockSupport.park()挂起。释放锁时:修改 state → 唤醒后继节点。" [citation:2][citation:3]
  • 追问 5:“StampedLock 的乐观读是什么?有什么使用限制?”

    高分回答

    "StampedLock的乐观读是一种无锁读取机制:

    1. 调用tryOptimisticRead()获取一个版本戳(stamp),不真正加锁;
    2. 拷贝需要读取的变量;
    3. 调用validate(stamp)检查 stamp 是否被写操作修改;
    4. 如果未修改,直接返回结果;如果修改了,升级为悲观读锁(readLock())重试。
      优势:纯读场景下性能接近无锁,远超ReentrantReadWriteLock
      限制
    5. 不可重入:同一线程重复获取会导致死锁;
    6. 不支持条件队列:没有newCondition()方法;
    7. stamp 必须验证:忘记validate()会导致读到脏数据;
    8. 写锁饥饿:乐观读不阻塞写线程,但大量乐观读可能导致写线程长期等待。" [citation:5]
  • 追问 6:“ReentrantReadWriteLock 的锁降级是什么?为什么需要?”

    高分回答

    "锁降级是指持有写锁的线程,在释放写锁前获取读锁,然后释放写锁,继续持有读锁。

    wl.lock();try{data=newValue;// 修改数据rl.lock();// 锁降级:写锁内获取读锁}finally{wl.unlock();// 释放写锁,此时仍持有读锁}try{use(data);// 使用数据,允许其他读线程并行}finally{rl.unlock();}

    为什么需要?

    1. 保证可见性:写锁释放后立即可见最新数据,但如果不持有读锁,其他写线程可能立即获取写锁修改数据;
    2. 降低锁粒度:持有读锁时允许其他读线程并行,提升并发度;
    3. 替代 volatile:某些场景下锁降级比 volatile + 写锁更安全。
      注意ReentrantReadWriteLock不支持锁升级(读锁内获取写锁会导致死锁)。" [citation:4]

10. 方案选型速查表
业务场景推荐方案核心理由
简单同步synchronized语法简洁,JVM 自动优化
需要超时/中断ReentrantLocktryLock/lockInterruptibly
需要公平锁ReentrantLock(true)避免饥饿
生产者消费者(多条件)ReentrantLock + Condition精确唤醒,避免惊群
读多写少ReentrantReadWriteLock读锁共享
读极多写极少StampedLock(乐观读)无锁读,性能最高
缓存(读多写少)ReentrantReadWriteLock锁降级保证可见性
高并发计数LongAdder分段累加,无锁

💡面试官想要的满分总结

Lock体系是 Java 并发编程从"语法糖"走向"精细化控制"的标志。理解 Lock 必须抓住三条主线:

第一条:AQS 框架ReentrantLockCountDownLatchSemaphore等几乎所有 JUC 同步组件都基于 AQS 实现。核心设计是state(同步状态)+ Node 队列(FIFO 等待队列)+ CAS + 自旋 + park/unpark。掌握 AQS 就掌握了 JUC 的半壁江山。

第二条:公平与非公平的权衡。非公平锁允许"插队"(先 CAS 尝试),吞吐量高但可能饥饿;公平锁严格 FIFO,避免饥饿但切换开销大。默认非公平是工程实践的最优解。

第三条:Condition 的多条件队列。与synchronized的单 waitSet 相比,Condition允许创建多个独立条件队列,实现精确唤醒(生产者只唤醒消费者),避免notifyAll()的惊群效应。

选型上,简单场景用synchronized,需要高级功能用ReentrantLock,读多写少用ReentrantReadWriteLock,读极多用StampedLock。永远记住:先保证正确性(finally 中 unlock),再追求性能(乐观读、锁降级)


觉得对您有帮助,麻烦点点关注啦,您的关注是我创作的最大动力~ 🎯

http://www.jsqmd.com/news/990954/

相关文章:

  • Arduino I2C通信避坑指南:手把手教你用Wire库实现双板联动(附电位器控制LED完整代码)
  • 用CH32X035做个“瑞士军刀”:PD/QC诱骗、ADC/DAC、电压电流计三合一保姆级教程
  • 如何免费解锁Wand专业版功能:告别2小时限制的终极解决方案
  • 别再手动做PPT了!用Python的win32com库批量生成100页演示文稿(附完整代码)
  • ESP32项目实战:手把手教你移植minizip库,实现本地文件解压(附完整代码)
  • AI Agent 状态机与工作流编排:从有限状态机到生产级编排引擎的设计实践
  • 计算机毕业设计之Django基于人脸识别的高校查寝小程序
  • 衡阳广受认可的政企活动策划公司客户口碑力荐 - myqiye
  • 2026泉州黄金变现指南:行情避坑技巧与三大优质回收门店推荐 - 润富黄金回收
  • 零象废品回收小程序V2.8.2完整开源包|含已修复登录功能的前后端代码与LNMP部署脚本
  • Shell文本处理与重定向
  • 手把手复现:用Python仿真5G NR的CPE估计与补偿流程(附代码解读)
  • 终极手机号码定位系统:3步实现免费地理位置查询
  • 突破传统文献管理:Zotero-GPT如何用AI重塑学术工作流
  • 2026年alloyc4排名,十大厂家 - myqiye
  • 用Raspberry Pi Pico做个便携MP3播放器:SD卡+I2S音频模块完整接线与代码解析
  • 3个维度重新定义AI项目部署:从容器化到云原生智能部署方案
  • 等保2.0倒计时!数据备份容灾新规,这5条硬指标你还没搞懂?
  • GuoFeng3古风AI绘画终极指南:从零开始掌握国风艺术创作
  • 解锁Wallpaper Engine资源宝库:RePKG专业解包与TEX转换全攻略
  • 遇到看不懂的报错信息?试试用 Claude 快速定位 Bug 的三个技巧 | 开发者避坑指南
  • Spring 零基础入门到进阶 JdbcTemplate 62-64
  • 2026 安徽黄山彩钢瓦翻新防水 TOP4 权威推荐(全区域服务 + 避坑指南) - 本地便民网
  • 2026年q2成都三相异步电机批发厂家实测评测:y系列电机生产厂家价格/y系列电机生产厂家推荐/优选指南 - 优质品牌商家
  • B站内容自动化监控终极指南:如何用Mirai插件实现UP主动态实时推送
  • 基于BERT微调的唐诗AI创作工具:支持随机写诗、诗句续写和藏头诗定制
  • Zapier AI 工作流编排平台
  • Apache CXF 3.1.18 命令行工具集:含 WSDL/Java 双向生成、JAX-WS/JAX-RS 运行支持与企业级安全组件
  • FPGA网络通信进阶:如何将你的UDP协议栈从RGMII PHY移植到SGMII+GT高速收发器方案?
  • 告别硬编码!用Qt TableWidget打造动态可配置的表格界面(附下拉框/复选框完整源码)