深入理解AQS:Java并发编程的核心基石

深入理解AQS:Java并发编程的核心基石 在Java并发编程中我们经常会用到ReentrantLock、Semaphore、CountDownLatch等并发工具却很少深入思考这些工具背后的共同支撑——AbstractQueuedSynchronizer简称AQS。它就像一个“万能骨架”定义了并发工具的核心逻辑是整个java.util.concurrent包的灵魂。今天我们就从AQS的基本概念出发结合ReentrantLock的源码的解析再聊聊自旋锁彻底搞懂这一并发核心机制。一、什么是AQSAQS的全称是AbstractQueuedSynchronizer即抽象队列同步器。它不是一个具体的同步工具而是一个抽象类提供了一套通用的机制来管理同步状态、阻塞/唤醒线程、维护等待队列。JUC下的大部分同步工具比如Lock系列ReentrantLock、ReentrantReadWriteLock和并发工具类Semaphore、CountDownLatch、CyclicBarrier都是基于AQS实现的相当于AQS为它们提供了“底层模板”开发者只需根据需求重写部分方法就能实现自定义的同步工具。AQS的核心设计思想AQS的核心逻辑围绕两个核心组件展开CLH同步队列和state状态属性二者协同工作实现线程的同步与调度。1. CLH同步队列CLH同步队列全称是Craig.Landin. and Haqersten lock queue是一个FIFO先进先出的双向队列主要用于存储被阻塞的线程信息。我们可以把它理解为一个“线程等待队列”当线程竞争资源失败时就会进入这个队列等待直到资源被释放后被唤醒继续竞争资源。CLH队列的两个关键特性决定了它的调度逻辑FIFO特性等待时间最长的线程会优先获得资源对应公平锁的排队逻辑保证了线程竞争的公平性双向队列支持线程从队列两端进行插入、删除和查找操作这为非公平锁的“插队”逻辑提供了可能。2. state状态属性state是AQS中的一个volatile成员变量用于表示共享资源的状态其具体含义由子类如ReentrantLock、CountDownLatch自行定义不同的同步工具对state的解读完全不同。举两个最常见的例子对于ReentrantLock可重入锁state0表示锁未被占用state1表示锁已被占用由于ReentrantLock支持重入state的值还会随着重入次数递增比如重入2次state2释放时则递减直到state0时完全释放锁。对于CountDownLatch倒计时器state的初始值为N表示需要等待N个线程完成每有一个线程完成任务state就减1当state0时所有阻塞的线程都会被唤醒继续执行。简单来说state就是AQS用来“标记资源是否可用”的核心变量通过对state的原子操作实现线程对资源的竞争与释放。二、ReentrantLock底层原理AQS的实际应用ReentrantLock是我们最常用的并发锁之一它的底层完全基于AQS实现支持公平锁和非公平锁两种模式。接下来我们就通过源码解析看看AQS是如何支撑ReentrantLock的锁机制的。2.1 公平锁与非公平锁的核心区别在聊源码之前我们先明确公平锁和非公平锁的核心差异公平锁严格按照线程请求锁的顺序分配锁线程必须排队等待不允许“插队”保证了所有线程的公平性非公平锁不严格按照请求顺序允许线程在“合适的时机”插队。这里的“合适时机”特指当前线程请求锁时恰好前一个持有锁的线程释放了锁此时当前线程可以直接获取锁无需排队。但如果锁正被占用当前线程依然会进入CLH队列等待并非完全随机插队。实际开发中非公平锁是ReentrantLock的默认模式因为它的吞吐量更高而公平锁适合对顺序性要求严格的场景比如银行转账、任务调度系统等能避免线程饥饿问题。2.2 ReentrantLock的内部结构ReentrantLock内部包含了3个与AQS相关的类它们的关系的是Sync抽象类继承自AQS是ReentrantLock的核心内部类定义了锁的基本逻辑NonfairSyncSync的子类实现了非公平锁的具体逻辑FairSyncSync的子类实现了公平锁的具体逻辑。也就是说ReentrantLock的公平与非公平特性本质上是通过Sync的两个子类实现的而这两个子类的核心差异就在于重写AQS的相关方法时是否遵循“排队顺序”。2.3 上锁流程从源码看AQS的工作机制我们分别从非公平锁和公平锁的角度解析ReentrantLock的上锁流程重点看AQS的核心方法如何被调用。1非公平锁的上锁流程非公平锁的上锁逻辑核心是“先尝试插队失败再排队”对应的源码如下① Sync抽象类的lock方法统一入口/** * 获取锁的操作如果初始尝试获取锁失败则进行阻塞式获取锁。 */ final void lock() { if (!initialTryLock()) // 如果初始尝试获取锁失败 acquire(1); // 调用AQS的方法进入阻塞获取逻辑 }② NonfairSync的initialTryLock方法非公平锁的初始尝试/** * 初始尝试获取锁。 * return 如果成功获取锁则返回true否则返回false。 */ final boolean initialTryLock() { Thread current Thread.currentThread(); // 1. 尝试用CAS将state从0改为1锁未被占用时直接获取 if (compareAndSetState(0, 1)) { setExclusiveOwnerThread(current); // 设置当前线程为锁的持有者 return true; } // 2. 如果当前线程已经持有锁重入state加1 else if (getExclusiveOwnerThread() current) { int c getState() 1; if (c 0) // 重入次数过多抛出异常 throw new Error(Maximum lock count exceeded); setState(c); return true; } // 3. 以上两种情况都失败返回false进入排队逻辑 else return false; }③ AQS的acquire方法阻塞获取锁的核心/** * 获取锁的操作。 */ public final void acquire(int arg) { if (!tryAcquire(arg)) // 再次尝试获取锁非公平锁的tryAcquire实现 acquire(null, arg, false, false, false, 0L); // 失败则进入队列阻塞 }④ NonfairSync的tryAcquire方法再次尝试获取锁/** * 尝试获取锁。 * return 如果成功获取锁则返回true否则返回false。 */ protected final boolean tryAcquire(int acquires) { // 再次尝试CAS修改state成功则获取锁 if (getState() 0 compareAndSetState(0, acquires)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; }总结非公平锁的上锁逻辑线程先尝试CAS获取锁插队如果锁未被占用直接获取如果自己已经持有锁就重入如果都失败就进入CLH队列等待。2公平锁的上锁流程公平锁的核心是“先检查队列再尝试获取锁”不允许插队其源码与非公平锁的差异主要在initialTryLock和tryAcquire方法① FairSync的initialTryLock方法/** * 初始尝试获取锁的操作。 * return 如果成功获取锁则返回true否则返回false。 */ final boolean initialTryLock() { Thread current Thread.currentThread(); int c getState(); if (c 0) { // 关键先判断是否有等待的线程!hasQueuedThreads()没有才尝试CAS if (!hasQueuedThreads() compareAndSetState(0, 1)) { setExclusiveOwnerThread(current); return true; } } else if (getExclusiveOwnerThread() current) { // 重入逻辑与非公平锁一致 if (c 0) throw new Error(Maximum lock count exceeded); setState(c); return true; } return false; }② FairSync的tryAcquire方法/** * 尝试获取锁的操作。 * return 如果成功获取锁则返回true否则返回false。 */ protected final boolean tryAcquire(int acquires) { // 关键增加了!hasQueuedPredecessors()判断是否有前驱节点是否有线程排队 if (getState() 0 !hasQueuedPredecessors() compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } return false; }这里的hasQueuedPredecessors()方法是公平锁的核心它会判断当前线程是否有前驱节点即队列中是否有比当前线程更早等待的线程。如果有就不允许获取锁必须排队如果没有才尝试CAS获取锁这就保证了公平性。2.4 解锁流程释放锁并唤醒等待线程解锁流程相对简单且公平锁和非公平锁的解锁逻辑完全一致核心是“释放锁修改state 唤醒队列中的下一个线程”源码如下① AQS的release方法解锁统一入口/** * 释放锁的操作。 * return 如果成功释放锁并且成功唤醒下一个等待线程则返回true否则返回false。 */ public final boolean release(int arg) { if (tryRelease(arg)) { // 尝试释放锁 signalNext(head); // 唤醒队列中的下一个等待线程 return true; } return false; }② Sync的tryRelease方法释放锁的具体逻辑/** * 尝试释放锁的操作。 * return 如果成功释放锁并且锁完全释放则返回true否则返回false。 */ ReservedStackAccess protected final boolean tryRelease(int releases) { int c getState() - releases; // state递减重入锁则多次递减 // 校验只有持有锁的线程才能释放锁 if (getExclusiveOwnerThread() ! Thread.currentThread()) throw new IllegalMonitorStateException(); boolean free (c 0); // 判断是否完全释放锁state0 if (free) setExclusiveOwnerThread(null); // 完全释放清空锁持有者 setState(c); // 更新state值 return free; // 返回是否完全释放 }③ AQS的signalNext方法唤醒下一个线程/** * 唤醒给定节点的后继节点如果存在并取消其WAITING状态 * 当一个或多个线程被取消时这可能无法唤醒一个合适的线程但cancelAcquire确保活性。 */ private static void signalNext(Node h) { Node s; // 校验头节点和后继节点是否有效 if (h ! null (s h.next) ! null s.status ! 0) { s.getAndUnsetStatus(WAITING); // 取消后继节点的等待状态 LockSupport.unpark(s.waiter); // 唤醒后继节点对应的线程 } }解锁流程总结线程释放锁时先将state递减若state减至0说明锁完全释放此时唤醒CLH队列中的下一个线程让其继续竞争锁。三、自旋锁AQS中的“轻量级等待”在聊AQS的过程中我们多次提到“线程阻塞”但实际上AQS在线程进入阻塞前会先尝试“自旋”获取锁——这就是自旋锁的核心思想。自旋锁是一种轻量级的同步机制也是AQS中线程竞争资源的一种优化策略。3.1 什么是自旋锁自旋锁的定义很简单当一个线程尝试获取锁时如果锁已经被其他线程占用该线程不会立即进入阻塞状态而是循环等待自旋不断判断锁是否可用直到获取到锁才退出循环。就像一个人在打印机前等待不离开而是每隔一会儿就检查打印机是否空闲这就是自旋的逻辑。自旋锁的核心特点是线程在等待过程中一直处于活跃状态不放弃CPU资源适合锁占用时间极短的场景。3.2 自旋锁的实现方式自旋的本质就是“无限循环”常见的实现方式有三种// 方式1for循环 for(;;){ // 不断尝试获取锁 } // 方式2while循环 while(true){ // 不断尝试获取锁 } // 方式3do-while循环 do{ // 不断尝试获取锁 }while(/* 锁未获取到 */);下面是一个结合ReentrantLock实现的自旋锁示例当尝试获取锁失败时通过递归调用自身实现自旋public AlbumInfo getAlbumInfo(Long id) { //创建锁 Lock lock new ReentrantLock(); try { // 尝试获取锁超时时间3秒 boolean tryLock lock.tryLock(3,TimeUnit.SECONDS); if(tryLock) { // 成功获取锁执行业务逻辑 AlbumInfo albumInfo getData(id); return albumInfo; } else { // 获取锁失败自旋重试 return getAlbumInfo(id); } }catch (Exception e) { // 异常处理 }finally { // 确保锁释放避免死锁 lock.unlock(); } }3.3 自旋锁与AQS的关联在AQS中自旋锁并不是一个独立的锁实现而是线程竞争资源时的一种“优化策略”对于ReentrantLock自旋的过程就是线程不断调用lock()方法反复尝试用CAS修改state值直到获取锁或自旋超时对于原子操作类如AtomicInteger自旋的过程就是CAS操作失败后再次尝试CAS操作直到成功修改值。3.4 自旋锁的优缺点自旋锁就像一把“双刃剑”有明显的优势和局限性实际使用时需要根据场景选择优点减少线程阻塞开销对于锁竞争不激烈、锁占用时间极短的场景自旋的消耗远小于线程阻塞-唤醒的开销线程阻塞需要切换到内核态唤醒需要切换回用户态开销较大提升性能避免了线程在内核态和用户态之间的切换能显著提升并发程序的运行效率。缺点浪费CPU资源如果锁竞争激烈或者锁占用时间较长自旋的线程会一直占用CPU做无用功导致其他线程无法获取CPU资源造成系统负载升高可能导致线程饥饿如果持有锁的线程长时间不释放锁自旋的线程会一直循环无法获取锁甚至可能无限期等待。因此自旋锁适合短时间、低竞争的场景如果锁竞争激烈或持有锁时间长应关闭自旋直接让线程进入阻塞状态。四、面试重点AQS底层原理总结结合前面的内容我们总结一下AQS的底层原理面试高频考点AQS的核心是“一个状态state 一个队列CLH双向队列 一套CAS机制”具体工作流程如下线程竞争资源时通过CAS操作修改state状态如果CAS操作成功state修改为目标值则线程获取到资源继续执行如果CAS操作失败线程会被加入CLH双向队列进入阻塞状态持有资源的线程释放资源时会修改state状态并唤醒队列中的下一个线程被唤醒的线程再次尝试CAS修改state重复上述流程。简单来说AQS的核心逻辑就是“抢状态→抢不到就排队→释放时唤醒下一个”这也是所有基于AQS的并发工具的共同底层逻辑。五、总结AQS作为Java并发编程的核心基石它的设计思想非常经典——通过抽象出同步机制的通用逻辑为各种并发工具提供统一的底层支撑减少了重复开发。理解AQS不仅能搞懂ReentrantLock、Semaphore等工具的底层原理更能帮助我们理解并发编程的核心逻辑在实际开发中更合理地选择和使用并发工具。最后提醒一句AQS的难点在于源码中的CAS操作、队列管理和线程阻塞/唤醒逻辑建议结合本文的解析对照JDK源码逐行阅读才能真正吃透这一核心机制。