- 教程
- 技术博客
- 文档
【免费下载链接】YCBlogs
技术博客笔记大汇总,包括Java基础,线程,并发,数据结构;Android技术博客等等;常用设计模式;常见的算法;网络协议知识点;部分flutter笔记;还包括平时开发中遇到的bug汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是markdown格式的!转载请注明出处,谢谢!
本篇是 YCBlogs 仓库 java/07.Java并发 系列中的核心一篇,围绕java.util.concurrent包下的互斥锁 ReentrantLock 展开:从"它是什么、比 synchronized 强在哪"讲起,给出可直接复制的标准使用模板与公平/非公平锁实测案例,并深入到Sync、FairSync、NonfairSync、Condition的源码级别,解释重入计数、hasQueuedPredecessors排队判断和条件队列的完整语义。读完你既能掌握 ReentrantLock 的正确打开方式(lock + try/finally + unlock),也能说清公平锁为何要用同步队列的第一个节点、重入锁为何要释放 n 次才真正解锁,以及它与 synchronized 究竟该怎么选。
01. ReentrantLock 是什么:比 synchronized 多出的三项高级能力
ReentrantLock 是java.util.concurrent包下提供的一套互斥锁。它实现了Lock接口,与synchronized关键字相比,ReentrantLock类提供了三项高级功能(详见原文档 06.ReentrantLock锁.md):
- 等待可中断:当持有锁的线程长期不释放锁时,正在等待的线程可以选择放弃等待、转而处理其他事情,这在某种程度上可以避免死锁。
synchronized做不到这一点——等待中的线程只能一直阻塞。 - 公平锁:多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁。
synchronized是非公平锁;ReentrantLock默认构造的也是非公平锁,但可以通过构造参数true显式开启公平锁。需要注意公平锁为了保证"先来先得"的绝对顺序,性能表现通常不如非公平锁。 - 锁绑定多个条件:一个
ReentrantLock对象可以同时绑定多个Condition对象,实现"一把锁 + 多组等待队列"的精细协作,而synchronized的wait/notify只能绑定一个隐含条件。
如果放到整个锁体系里看,ReentrantLock 属于仓库 03.锁一些基础概念.md 中归纳的"悲观锁、可重入锁、独享锁、可中断锁"这一类:它假定共享数据一定会被并发修改,所以通过显式加锁来保证线程安全;同一线程可重复获取;同一时刻只允许一个线程持有;并支持响应中断的获取方式。
02. ReentrantLock 使用方法:标准模板与注意事项
2.1 最小可用代码
原文档给出了最基础的使用方式,核心是lock() 与 unlock() 必须成对出现,且解锁动作一定要放在finally中,保证无论业务代码是否抛异常,锁都能被释放:
private ReentrantLock lock = new ReentrantLock(); public void run() { lock.lock(); try { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); } } finally { lock.unlock(); } }2.2 三个必须遵守的纪律
- 每个
lock()动作,都建议立即对应一个try-catch-finally(原文档原文强调:"为保证锁释放,每一个 lock() 动作,建议都立即对应一个 try-catch-finally")。 - 不要将获取锁的过程写在
try块内:如果在获取锁时发生了异常,异常抛出的同时会让人误以为锁被释放了,实际上锁根本未被持有,容易掩盖真实问题。正确姿势是先lock.lock(),再进入try。 - 在
finally中释放锁:这是使用ReentrantLock时最重要的原则,目的就是保证"获取锁之后,最终一定能被释放"。synchronized发生异常时 JVM 会自动释放锁,而Lock不会,忘记unlock()极可能造成死锁。
这两条纪律在仓库的对比文档 08.两种不同锁区别.md 第 06 节"注意事项"中同样被重点强调,属于使用 ReentrantLock 的第一道安全底线。
2.3 从源码看 lock 与 unlock 的委托关系
ReentrantLock本身并没有直接实现锁竞争逻辑,而是通过组合模式把lock/unlock委托给内部同步器Sync完成(完整类结构见 07.ReentrantLock原理.md):
public class ReentrantLock implements Lock, java.io.Serializable { private final Sync sync; // 组合一个 Sync 同步器 public void lock() { sync.lock(); // 委托给 Sync 的子类实现 } public void unlock() { sync.release(1); // 通过 AQS 的 release 释放 } public Condition newCondition() { return sync.newCondition(); // 创建绑定该锁的条件变量 } }其中abstract static class Sync extends AbstractQueuedSynchronizer(AQS)。也就是说,ReentrantLock 的全部同步语义(获取、释放、排队、重入计数)都是通过重写 AQS 的若干 protected 方法来表达的——这正是后面第 05、06 节源码分析的入口。Doug Lea 正是用这种"组合 + 委托"的方式,把易用性(面向用户的 Lock API)与灵活性(面向同步器的 AQS 扩展点)解耦开。
03. ReentrantLock 锁机制测试:验证互斥效果
原文档提供了一个完整的测试,两个线程t1、t2共享同一个Runnable实例(从而共享同一把锁),观察输出可以直观验证互斥性:
private void test2() { Runnable t1 = new MyThread(); new Thread(t1, "t1").start(); new Thread(t1, "t2").start(); } class MyThread implements Runnable { private ReentrantLock lock = new ReentrantLock(); public void run() { lock.lock(); try { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); } } finally { lock.unlock(); } } }运行结果(原文档实测日志):
10-17 17:06:59.222 6531-6846/com.yc.cn.ycbaseadapter I/System.out: t1:0 10-17 17:06:59.222 6531-6846/com.yc.cn.ycbaseadapter I/System.out: t1:1 10-17 17:06:59.222 6531-6846/com.yc.cn.ycbaseadapter I/System.out: t1:2 10-17 17:06:59.222 6531-6846/com.yc.cn.ycbaseadapter I/System.out: t1:3 10-17 17:06:59.222 6531-6846/com.yc.cn.ycbaseadapter I/System.out: t1:4 10-17 17:06:59.224 6531-6847/com.yc.cn.ycbaseadapter I/System.out: t2:0 10-17 17:06:59.225 6531-6847/com.yc.cn.ycbaseadapter I/System.out: t2:1 10-17 17:06:59.225 6531-6847/com.yc.cn.ycbaseadapter I/System.out: t2:2 10-17 17:06:59.225 6531-6847/com.yc.cn.ycbaseadapter I/System.out: t2:3 10-17 17:06:59.225 6531-6847/com.yc.cn.ycbaseadapter I/System.out: t2:4可以看到t1完整打印完 0~4 之后,t2才获得执行机会——因为两线程竞争同一把ReentrantLock,t1持锁期间t2只能在 AQS 同步队列中阻塞等待,t1调用unlock()(内部走sync.release(1))释放后,t2才被唤醒并重新竞争锁。这也印证了第 02 节"lock/unlock 必须成对、finally 兜底"的必要性:一旦持锁线程异常退出且没有释放锁,t2将永远阻塞。
04. 何时用 ReentrantLock:场景判断与选型建议
4.1 适用场景
原文档明确给出的适用场景是:
- 时间锁等候(带超时的锁获取)
- 可中断锁等候(等待可被中断)
- 无块结构锁(锁的获取与释放跨越多个代码块/方法)
- 多个条件变量(一把锁绑定多个 Condition)
- 锁投票
概括一句话:在确实需要一些 synchronized 所没有的特性的时候,才考虑使用 ReentrantLock。
4.2 如何选用:先用 synchronized,直到证明它不合适
原文档的选型建议非常务实,核心观点是:
ReentrantLock 具有可伸缩性的好处,应当在高度争用的情况下使用它;但大多数 synchronized 块几乎从来没有出现过争用。建议用 synchronized 开发,直到确实证明 synchronized 不合适,而不要仅仅假设使用 ReentrantLock"性能会更好"。这些是供高级用户使用的高级工具,首先要把事情做好,然后再考虑是否有必要做得更快。
这一点在仓库 08.两种不同锁区别.md 中也有完全一致的表述,且补充了性能背景:JDK 1.5 时代 synchronized 是重量级操作,挂起/恢复线程需要用户态与内核态切换,性能压力大;到 JDK 1.6,JVM 引入了自适应自旋、锁消除、锁粗化、轻量级锁、偏向锁等优化(详见 05.Synchronize原理.md 的锁升级机制讲解),synchronized 的性能已不比 Lock 差,官方也更倾向 synchronized,并在未来版本持续优化它。因此,能用 synchronized 满足需求时,优先用 synchronized。
4.3 真实工业案例:ThreadPoolExecutor 中的 mainLock
原文档特意摘取了ThreadPoolExecutor.addWorkerFailed方法作为 ReentrantLock 的工业级使用范例,说明"这个类中很多地方用到了这个锁":
/** * Rolls back the worker thread creation. * - removes worker from workers, if present * - decrements worker count * - rechecks for termination, in case the existence of this * worker was holding up termination */ private void addWorkerFailed(Worker w) { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { if (w != null) workers.remove(w); decrementWorkerCount(); tryTerminate(); } finally { mainLock.unlock(); } }这个例子至少体现三点实战要点:其一,先把mainLock存入局部变量(减少一次字段读取);其二,lock()在try之外;其三,unlock()严格放在finally中。线程池对内部状态(workers集合、worker 计数)的并发保护正是借助这把 ReentrantLock 完成的,是"无块结构锁 + 高争用内部状态保护"的典型场景。
05. 公平锁与非公平锁:从构造器到 hasQueuedPredecessors
5.1 什么是公平性
公平性是指在竞争场景中,当公平性为真时,锁会倾向于赋予等待时间最久的线程,它是减少线程"饥饿"(个别线程长期等待锁却始终无法获取)的办法:
- 公平锁:老的线程排队使用锁,新线程仍然排队使用锁——严格按申请顺序来;
- 非公平锁:老的线程排队使用锁,但无法保证新线程不抢占已经排队的线程的锁。
何谓公平性,是针对获取锁而言的:如果一个锁是公平的,那么锁的获取顺序应该符合请求上的绝对时间顺序,满足FIFO。
5.2 两个构造方法对应的源码
// 无参构造:默认非公平锁 public ReentrantLock() { sync = new NonfairSync(); } // 带 boolean 构造:true 为公平锁,false 为非公平锁 public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }FairSync与NonfairSync都是Sync的子类,各自重写了lock()与tryAcquire(int),从而表达不同的同步语义。
5.3 公平锁的获取逻辑:多了一个 hasQueuedPredecessors
原文档给出的公平锁核心方法:
protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }这段逻辑与nonfairTryAcquire(见第 06 节)几乎一致,唯一的不同在于增加了hasQueuedPredecessors()判断:该方法用于判断当前节点在同步队列中是否有前驱节点——如果有,说明有线程比当前线程更早请求资源,根据公平性,当前线程必须获取失败、继续排队;只有当前节点没有前驱节点时,才值得继续做 CAS 抢锁。
由此可以得出关键结论:
- 公平锁每次都是从同步队列中的第一个节点获取到锁,即严格按 FIFO 顺序放行;
- 非公平锁则不一定,有可能刚释放锁的线程能立刻再次获取到锁(因为它刚释放完,紧接着就尝试 CAS,此时同步队列里的老线程还没来得及被唤醒参与竞争),从而造成"饥饿"风险。
仓库 03.锁一些基础概念.md 第 1.3 节对两者的取舍给出了更系统的总结:公平锁保证请求资源的绝对时间顺序,但要频繁切换上下文;非公平锁能减少上下文切换、保证更大的系统吞吐量。这正是 ReentrantLock 默认选择非公平锁的原因——用少量的"插队"换取整体吞吐。
5.4 公平 / 非公平锁实测案例
原文档的测试案例:10 个线程(Thread-5 ~ Thread-14)共享同一个Service中的锁,先各自打印"抢到了锁",再尝试lock.lock()并打印"已经被锁定"。
private void test3() { Service service = new Service(); ThreadClass tcArray[] = new ThreadClass[10]; for (int i = 0; i < 10; i++) { tcArray[i] = new ThreadClass(service); tcArray[i].start(); } } public class Service { ReentrantLock lock = new ReentrantLock(true); // true = 公平锁 Service() { } void getThreadName() { System.out.println(Thread.currentThread().getName() + " 已经被锁定"); } } public class ThreadClass extends Thread { private Service service; ThreadClass(Service service) { this.service = service; } public void run() { System.out.println(Thread.currentThread().getName() + " 抢到了锁"); service.lock.lock(); service.getThreadName(); service.lock.unlock(); } }公平锁(new ReentrantLock(true))实测输出(节选):
Thread-5 抢到了锁 Thread-5 已经被锁定 Thread-6 抢到了锁 Thread-6 已经被锁定 Thread-7 抢到了锁 Thread-8 抢到了锁 Thread-7 已经被锁定 Thread-8 已经被锁定 Thread-9 抢到了锁 Thread-9 已经被锁定 Thread-10 抢到了锁 Thread-10 已经被锁定 Thread-11 抢到了锁 Thread-11 已经被锁定 Thread-12 抢到了锁 Thread-12 已经被锁定 Thread-14 抢到了锁 Thread-14 已经被锁定 Thread-13 抢到了锁 Thread-13 已经被锁定非公平锁(new ReentrantLock(false))实测输出(节选):
Thread-5 抢到了锁 Thread-6 抢到了锁 Thread-5 已经被锁定 Thread-7 抢到了锁 Thread-7 已经被锁定 Thread-5 已经被锁定 -> 实际为 Thread-5/6/7 交错 Thread-6 已经被锁定 ...对比可以得出结论:公平锁下,哪个线程先运行、先发出申请,就能按顺序得到锁(输出中 "抢到了锁" 与 "已经被锁定" 基本保持先到先得的次序);非公平锁下,新线程可能抢占已经在排队的线程的锁(多个线程"抢到了锁"的交错出现,谁先抢到完全看 CAS 运气)。这也直接呼应了hasQueuedPredecessors的源码语义——公平锁把"排队顺序"硬编码进了获取逻辑,非公平锁则跳过这一步直接竞争。
06. 重入性(ReentrantLock 重入锁):state 计数与完全释放
6.1 什么是重入锁
ReentrantLock 是Lock接口的实现类,也是实际编程中使用频率很高的一个锁,支持重入性(Reentrant)——即当前线程获取该锁之后,再次获取不会被阻塞,能够对共享资源重复加锁。
- Java 关键字
synchronized也隐式支持重入:通过"获取自增、释放自减"的方式实现(详见 04.Synchronize细说.md 第 4.1 节对monitorenter/monitorexit计数器的讲解——每个对象维护一个被锁次数计数器,获取加 1,释放减 1,归零才真正释放); - 与此同时,ReentrantLock 还支持公平锁和非公平锁两种方式。
要完全弄懂 ReentrantLock,核心就是理解它的同步语义,共两条:
- 重入性的实现原理;
- 公平锁与非公平锁(已在上节讲完)。
6.2 重入要解决的两个问题
要支持重入性,必须解决两个问题:
- 线程获取锁时,如果已经持有锁的线程就是当前线程,则直接再次获取成功;
- 锁被获取 n 次,就必须被释放同样的 n 次,锁才算完全释放成功。
同步组件通过重写 AQS 的 protected 方法(tryAcquire/tryRelease)来表达这两条语义。
6.3 获取:nonfairTryAcquire 的计数逻辑
以非公平锁为例,判断当前线程能否获得锁的核心方法是nonfairTryAcquire:
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); // 1. 如果该锁未被任何线程占有,该锁能被当前线程获取 if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } // 2. 若被占有,检查占有线程是否是当前线程 else if (current == getExclusiveOwnerThread()) { // 3. 再次获取,计数加一 int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }逻辑拆解:
- 第一步:锁空闲(
state == 0)时,通过CAS把同步状态从 0 改为acquires(通常为 1),成功则独占,setExclusiveOwnerThread记录持有线程; - 第二步:锁已被占用时,检查占用线程是否为当前线程——这是重入的关键。如果是,把同步状态
state加 1 并返回true,表示当前线程再次获取成功; - 若状态被加爆(
nextc < 0,即 int 溢出),抛出Error("Maximum lock count exceeded")。
可见,每次重入都会对同步状态做加一操作,state的值语义就是"当前持有线程的重入次数"。
6.4 释放:tryRelease 的递减与归零判断
释放的处理思路(依然以非公平锁为例),核心方法是tryRelease:
protected final boolean tryRelease(int releases) { // 1. 同步状态减 1 int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { // 2. 只有当同步状态为 0 时,锁成功被释放,返回 true free = true; setExclusiveOwnerThread(null); } // 3. 锁未被完全释放,返回 false setState(c); return free; }要点:
- 释放前先校验
Thread.currentThread() == getExclusiveOwnerThread(),否则抛IllegalMonitorStateException——防止非持锁线程"替别人解锁"; - 同步状态减
releases(通常为 1),只有减到 0 时才把独占线程置空,返回true表示完全释放; - 如果锁被获取 n 次、只释放了 n-1 次,
state仍大于 0,返回false,说明锁尚未完全释放,其他线程依然无法获取。
到这里就理清了 ReentrantLock 重入性的完整实现:获取时计数加一,释放时计数减一,计数归零才真正交出锁。外层sync.release(1)(对应unlock())正是先走tryRelease判定,再据此决定是否唤醒后继等待线程。
07. 锁绑定多个条件:Condition 与生产者-消费者实战
7.1 Condition 解决了什么问题
条件变量很大程度是为了解决Object.wait/notify/notifyAll难以使用的问题。在synchronized中,所有线程都挤在同一个 object 的条件队列上等待,区分不同等待原因非常麻烦;而ReentrantLock中,每个Condition都独立维护一个条件队列,一把锁可以绑定任意多个Condition。
对应关系如下:
| Condition 方法 | Object 方法 | 说明 |
|---|---|---|
await() | wait() | 释放锁并挂起,等待被唤醒 |
signal() | notify() | 唤醒条件队列中的一个等待线程 |
signalAll() | notifyAll() | 唤醒条件队列中的全部等待线程 |
Condition改名的目的,正是为了避免与Object的wait/notify/notifyAll在语义和使用上混淆。每一个Condition都与Lock绑定,因此继承了 Lock 的公平性特性:公平锁下,线程按 FIFO 顺序从Condition.await中释放;非公平锁下,后续锁竞争不保证 FIFO。
7.2 生产者-消费者中的多条件实践
原文档给出了一个基于Condition的有界队列(生产/消费两个条件变量),完整代码见 07.ReentrantLock原理.md,核心思路如下:
public class Queue<T> { private final T[] items; private final Lock lock = new ReentrantLock(); private Condition notFull = lock.newCondition(); // 不满:可继续 put private Condition notEmpty = lock.newCondition(); // 不空:可继续 take private int head, tail, count; public void put(T t) throws InterruptedException { lock.lock(); try { while (count == items.length) { notFull.await(); // 数组满,生产者挂起等待 } items[tail] = t; if (++tail == items.length) tail = 0; ++count; notEmpty.signal(); // 唤醒一个等待的消费者 } finally { lock.unlock(); } } public T take() throws InterruptedException { lock.lock(); try { while (count == 0) { notEmpty.await(); // 数组空,消费者挂起等待 } T o = items[head]; items[head] = null; // 置空便于 GC if (++head == items.length) head = 0; --count; notFull.signal(); // 唤醒一个等待的生产者 return o; } finally { lock.unlock(); } } }与单一条件队列方案相比,notFull/notEmpty两个条件变量的价值在于:满时只挂起生产者、空时只挂起消费者,互相唤醒互不干扰——这是synchronized单条件队列很难优雅做到的。
7.3 await / signal 的底层机制
假设线程 A、B 并发往队列插入数据,当数组存满时,线程 A 获取到锁并执行await()。ConditionObject.await的核心流程(原文档源码摘录):
addConditionWaiter():把线程 A 封装成Node.CONDITION状态的节点,加入条件等待队列(若队尾是取消状态节点,先清理);fullyRelease(node):线程 A释放锁,实质是把 AQS 的state置 0,并唤醒 AQS 同步队列中的后继线程(如 B),B 被唤醒后重新竞争锁;- 若线程 A 已不在同步队列中,则通过
LockSupport.park(this)挂起自己,等待被唤醒; - 被唤醒后走
acquireQueued(node, savedState)重新竞争锁:失败继续挂起,成功则从await()位置恢复执行(并恢复之前保存的重入计数savedState)。
相应地,线程 B 持锁执行take()并调用signal()时(源码见 07.ReentrantLock原理.md):
- 取出条件队列中第一个非 CANCELLED 节点(即线程 A;
signalAll则唤醒所有非 CANCELLED 节点,遇到取消节点顺手清理); - 通过
transferForSignal用CAS 把节点waitStatus从 CONDITION 改为 0,再调用enq(node)把线程 A插入 AQS 同步队列尾部; - 视前驱节点状态决定是否
LockSupport.unpark(node.thread)唤醒线程 A,让 A 与其他线程一起公平(或非公平)竞争锁。
简而言之:await 是"入条件队列 → 释放锁 → park 挂起";signal 是"出条件队列 → 入同步队列 → 可能 unpark 唤醒"。理解这套流转,就能把 ReentrantLock 的"锁 + 条件"体系与 AQS 的同步队列、Condition 的条件队列两套队列彻底打通。
08. ReentrantLock 与 synchronized 全面对比
仓库 08.两种不同锁区别.md 给出了两者非常系统的对比,结合本篇内容汇总如下:
8.1 相似点
两者都是加锁方式同步、阻塞式同步:当某个线程获得对象锁并进入同步块后,其他访问该同步块的线程必须阻塞在同步块外等待。而线程阻塞与唤醒的代价较高(操作系统需要在用户态与内核态之间来回切换),这也是 JVM 后来引入锁优化改善 synchronized 的原因。
8.2 API 层面
synchronized是Java 语言关键字,属于原生语法层面的互斥,由JVM 实现:同步代码块编译后生成monitorenter/monitorexit字节码指令,同步方法则带有ACC_SYNCHRONIZED标志(详见 04.Synchronize细说.md);ReentrantLock是JDK 1.5 之后提供的 API 层面的互斥锁,需要lock()与unlock()方法配合try/finally语句块手动完成;synchronized既可修饰方法也可修饰代码块;Lock只适用于代码块锁,但使用更灵活。
8.3 等待可中断
- synchronized:若持有锁的线程不释放,等待线程只能一直等待,不能被中断;
- ReentrantLock:等待线程可以
lockInterruptibly()等方式响应中断,等待很久后可放弃等待转做其他事,对执行时间很长的同步块非常有帮助。
8.4 公平性
synchronized的锁是非公平锁;ReentrantLock默认也是非公平锁,但可通过带 boolean 的构造函数要求公平锁(true公平、false非公平)。公平锁按请求顺序依次获得锁但有成本;非公平锁允许"讨价还价"(抢占)。
8.5 锁绑定多个条件
ReentrantLock可同时绑定多个Condition,多次调用newCondition()即可;synchronized中锁对象的wait()/notify()/notifyAll()只能实现一个隐含条件,要与多个条件关联时,就不得不额外添加锁。
8.6 异常与锁的释放
synchronized:发生异常时自动释放线程占有的锁,因此不会因异常导致死锁;Lock:发生异常时若没有主动unlock(),很可能造成死锁,所以必须在finally中释放锁。
8.7 其他能力
- 通过
Lock可以知道有没有成功获取锁(tryLock()返回 boolean),而synchronized无法办到; Lock还具备尝试非阻塞获取锁、可中断获取锁、超时获取锁(指定时间范围内获取,超时返回)等特性;Lock可以提高多个线程进行读操作的效率(配合读写锁体系使用)。
8.8 性能结论
- 竞争不激烈时,两者性能差不多;
- 竞争非常激烈(大量线程同时竞争)时,ReentrantLock 的性能优于 synchronized;
- JDK 1.5 中 synchronized 是重量级操作(挂起/恢复线程需转入内核态),性能低效;JDK 1.6 加入自适应自旋、锁消除、锁粗化、轻量级锁、偏向锁等优化后,synchronized 性能已不逊于 Lock。
因此最终选型建议是:在 synchronized 能满足需求的情况下,优先考虑 synchronized;只有当确实需要可中断、可超时、多条件、公平锁等 synchronized 不具备的特性,或处于高争用场景需要更高吞吐时,才引入 ReentrantLock。
09. 小结:ReentrantLock 知识清单
| 主题 | 核心要点 |
|---|---|
| 本质 | java.util.concurrent下的互斥锁,实现Lock接口,基于 AQS |
| 三大高级特性 | 等待可中断、公平锁、锁绑定多个条件 |
| 使用纪律 | lock()在 try 外、unlock()在 finally 中、成对出现 |
| 重入性 | 获取state+1,释放state-1,归零才完全释放;setExclusiveOwnerThread记录持有者 |
| 公平锁 | 构造传true;tryAcquire中hasQueuedPredecessors()保证 FIFO,每次从同步队列第一个节点拿锁 |
| 非公平锁 | 默认实现;跳过排队检查直接 CAS 抢占,吞吐更高但可能造成饥饿 |
| Condition | 对应Object.wait/notify/notifyAll;多条件队列实现精细的生产者-消费者协作 |
| 与 synchronized | API 层互斥 vs JVM 关键字;异常自动释放 vs 必须 finally 释放;可中断/可超时 vs 不可中断;高争用下 ReentrantLock 更有优势 |
本文是 YCBlogs 仓库 java/07.Java并发 系列中关于显式锁的完整一篇,可与同目录下 03.锁一些基础概念.md(锁的分类全景)、07.ReentrantLock原理.md(完整源码与 Condition 机制)、08.两种不同锁区别.md(与 synchronized 系统对比)、04.Synchronize细说.md(synchronized 用法与原理)配合阅读,即可形成从关键字锁到显式锁、从使用到 AQS 源码的完整知识闭环。
- 教程
- 技术博客
- 文档
【免费下载链接】YCBlogs
技术博客笔记大汇总,包括Java基础,线程,并发,数据结构;Android技术博客等等;常用设计模式;常见的算法;网络协议知识点;部分flutter笔记;还包括平时开发中遇到的bug汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是markdown格式的!转载请注明出处,谢谢!
相关推荐
JCSprout 源码解读:ReentrantLock 实现原理(AQS 公平锁与非公平锁全解析)
JCSprout 源码解读:ReentrantLock 实现原理(AQS 公平锁与非公平锁全解析) ReentrantLock 是 JDK 提供的显式可重入锁,
文档知识库后端教程YCBlogs Java 并发专题:ReentrantLock 原理深度解析——从 AQS 委托、重入语义到 Condition 条件队列
YCBlogs Java 并发专题:ReentrantLock 原理深度解析——从 AQS 委托、重入语义到 Condition 条件队列 本文是 YCBlog
教程技术博客文档JCSprout 源码级详解:ReentrantLock 实现原理与 AQS 公平锁/非公平锁机制
JCSprout 源码级详解:ReentrantLock 实现原理与 AQS 公平锁/非公平锁机制 ReentrantLock 是 Java 并发编程中除 sy
文档知识库后端教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考