前言:有了 Synchronized,为什么还要造出 ReentrantLock?
在上篇博客中我们提到,经过优化的synchronized已经很强了。但是,JDK 大神 Doug Lea(Java 并发包 JUC 之父) 依然为我们提供了一把神兵利器:ReentrantLock。
既然synchronized能自动加锁解锁,为什么大佬还要我们手动去lock()和unlock()呢?
因为synchronized太死板了!它不能被打断、不能超时等待、也不能实现公平排队。要想拥有绝对的并发火力和灵活性,我们必须了解 JUC 的镇山之宝 ——AQS (AbstractQueuedSynchronizer)。
一、 AQS 到底是个什么鬼?(通俗解释)
AQS 全称:抽象队列同步器。
你不需要被这个高大上的名字吓到,剥开它的外衣,AQS 本质上就是两个东西的结合体:
AQS =volatile int state(状态变量) +双向 FIFO 链表(等候队列)
小白图景解释:
你可以把 AQS 想象成银行办理业务的大厅。
- state(计数器):就是大厅里那个 VIP 柜台的状态灯。
0代表闲置,1代表里面有人正在办理业务。(如果是可重入锁,那个人反复进出,状态就是 2、3、4…) - 双向链表(Node 队列):就是柜台外面的排队等候区座位。如果新来的客户看到柜台(state = 1)有人,大厅保安(AQS机制)就会给他建一张档案卡(Node节点),并安排他坐到椅子上睡觉(线程休眠
LockSupport.park()),等前面的人办完业务走了,再去碰醒排在最前面的客户。
绝大部分 Java 并发包里的锁(ReentrantLock、CountDownLatch、Semaphore、读写锁),底层全是靠继承了 AQS 来实现的!只要你懂了 AQS,大半个并发包就被你拿下了。
二、 剖析 ReentrantLock 的“灵活”
ReentrantLock这个名字直译叫可重入锁。所谓的灵活,其实就在于它提供的“公平”与“非公平”的选择权。
1. 公平锁(FairSync)与非公平锁(NonfairSync)
在ReentrantLock内部,有两个基于 AQS 的实现类:
- 非公平锁(默认默认默认!):新来的线程不管三七二十一,一上来直接通过 CAS 尝试把 AQS 里的
state改为 1。如果正好前面那个刚出来,锁空出来了,新线程直接插队成功抢走锁!不管等候队列里有多少人在风中发抖。这种粗暴的做法反而效率最高,因为减少了线程唤醒的开销。 - 公平锁:新来的线程非常有素质。抢之前先看一眼 AQS 的等待队列。如果有任何人在自己前面排队,那就老老实实自己走到队尾去睡觉排队。杜绝了“饿死”现象,但整体吞吐量低。
三、 代码深度拆解(逐行注释)
我们用一段日常都在用的代码,来剖析到底底层发生了什么:
importjava.util.concurrent.locks.ReentrantLock;publicclassReentrantLockAqsDemo{// 默认传入 false,或者不传参,就是【非公平锁】// 传入 true,就是【公平锁】privatestaticfinalReentrantLocklock=newReentrantLock(false);publicstaticvoidmain(String[]args){// 创建三个线程来模拟并发抢锁的过程Threadt1=newThread(ReentrantLockAqsDemo::doBusiness,"小白");Threadt2=newThread(ReentrantLockAqsDemo::doBusiness,"小红");t1.start();t2.start();}publicstaticvoiddoBusiness(){// ========== 【AQS 加锁流程触发】 ==========/* 1. 线程如果是非公平模式,第一步直接去强行 CAS 把 AQS.state 从 0 变成 1。 2. 如果成功了,就在 AQS 里记录 ownerThread 为当前线程,美滋滋去执行业务。 3. 如果发现 state 是 1 已经被占了,也没事。看一眼霸占的人是不是自己。如果是自己,就把 state 加 1 (也就是变成了 2)。这叫【可重入】。 4. 如果都被别人占了,得,进入 AQS 的 acquire 方法,被包装成 Node 扔进双向链表的队尾挂起(park)。 */lock.lock();try{System.out.println(Thread.currentThread().getName()+" -> 获取到了ReentrantLock锁,正在执行业务!");// 模拟复杂业务Thread.sleep(2000);}catch(InterruptedExceptione){e.printStackTrace();}finally{// ========== 【AQS 解锁流程触发】 ==========/* 1. 谁加的锁谁才能解!先判断当前线程是不是 AQS 记录的 ownerThread。 2. 紧接着把 AQS 的 state 减 1。 3. 如果 state 减 1 之后变成 0 了,说明锁彻底释放干净了。 4. AQS 会从双向链表的头部,找到第一个真正还在沉睡排队的 Node,去 unpark() 唤醒他。小弟起来干活拿锁了! */System.out.println(Thread.currentThread().getName()+" -> 业务执行完毕,准备释放锁...");lock.unlock();}}}四、 面试神仙大乱斗:ReentrantLock vs Synchronized
搞懂了底层之后,面对面试官这道世纪对决题就非常从容了:
- 底层原理不同:
Synchronized是 JVM 层面的关键字,基于对象头的 Mark Word 和系统层面的 Monitor 实现。ReentrantLock是 JDK API 层面的类,纯 Java 代码,完全基于 AQS 这个状态机和等待队列来实现。
- 灵活性不同:
Synchronized只能被动阻塞,死等。非公平。ReentrantLock可以tryLock()(拿不到就马上拉倒返回 false 不死等)、可以带超时等待、更可以设置公平锁与非公平锁,甚至可以通过Condition定制化等待唤醒通道。
- 释放方式不同:
Synchronized执行完大括号,或者抛出异常时,JVM 会自动帮你释放锁。ReentrantLock必须要程序员自己在finally代码块强制调用unlock()!!(千万不能忘,否则就是坑爹的死锁)
总结:该用哪个?
在 JDK 1.6 之前毫无疑问用 ReentrantLock,因为那时 synchronized 太慢了。但现在官方已经充分优化了 synchronized。日常最常见的同步需求,优先使用synchronized,代码更简洁不容易漏写释放。
当你需要跨越方法的加解锁、试图中断排队线程、或者需要公平机制时,大胆掏出你的重武器 —— ReentrantLock 吧!