剥开 AQS 的外衣:ReentrantLock 凭什么这么灵活?
前言有了 Synchronized为什么还要造出 ReentrantLock在上篇博客中我们提到经过优化的synchronized已经很强了。但是JDK 大神 Doug LeaJava 并发包 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就是【公平锁】privatestaticfinalReentrantLocklocknewReentrantLock(false);publicstaticvoidmain(String[]args){// 创建三个线程来模拟并发抢锁的过程Threadt1newThread(ReentrantLockAqsDemo::doBusiness,小白);Threadt2newThread(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 吧