面试官:说下你对AQS的理解!

AQS,是 AbstractQueuedSynchronizer(抽象队列同步器)这个类的简称,也是 Java JUC 包中的灵魂,ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch、CyclicBarrier 都是通过其实现锁或同步器的。
首页 新闻资讯 行业资讯 面试官:说下你对AQS的理解!

在技术面试的时候,“说下你对 AQS 的理解”,这个问题出现的概率属实不低,而一些技术底子一般的同学,又非常容易被它复杂的底层源码弄得晕头转向。

今天这篇文章,我们就以做减法的方式,将这个知识点彻底地大家讲清楚。

AQS,是 AbstractQueuedSynchronizer(抽象队列同步器)这个类的简称,也是 Java JUC 包中的灵魂,ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch、CyclicBarrier 都是通过其实现锁或同步器的。

其核心思想为,在多线程并发访问共享资源时,通过双向链表实现的先进先出 CLH 队列进行线程排队,并通过由 volatile 修饰的 state 变量来标识资源的锁占用状态。

如下图所示:

图片图片

在 AQS 中提供了两种资源获取方式:

独占模式(Exclusive),在同一时刻只能有一个线程获取竞态资源,比如:ReentrantLock。

共享模式(Share),在同一时刻可以有多个(参数设定)线程获取竞态资源,比如:CountDownLatch、Semaphore。

AQS 方法详述

AQS 的方法大致分为三类,分别是独占模式下的方法、共享模式下的方法、通过模板方法模式留给子类实现的方法。

独占模式:

// 获取锁publicfinal void acquire(intarg)// 以可中断的方式获取锁publicfinal void acquireInterruptibly(intarg)// 以带超时时间的方式,尝试获取锁publicfinalbooleantryAcquireNanos(intarg,long nanosTimeout)// 释放锁publicfinalbooleanrelease(intarg)

共享模式:

// 获取锁publicfinal void acquireShared(intarg)// 以可中断的方式获取锁publicfinal void acquireSharedInterruptibly(intarg)// 以带超时时间的方式,尝试获取锁publicfinalbooleantryAcquireSharedNanos(intarg,long nanosTimeout)// 释放锁publicfinalbooleanreleaseShared(intarg)

需要子类实现的方法:

// 尝试获取独占锁protectedbooleantryAcquire(intarg);// 尝试释放独占锁protectedbooleantryRelease(intarg);// 尝试获取共享锁protectedinttryAcquireShared(intarg);// 尝试释放共享锁protectedbooleantryReleaseShared(intarg);// 判断当前线程是否正在持有锁protectedbooleanisHeldExclusively();

看到 AQS 父类实现了一部分方法,也预留了一些方法让 ReentrantLock、CountDownLatch、Semaphore、CyclicBarrier 等子类实现,我们想到了哪种设计模式?

是的,模板方法模式。

模板方法模式:定义一个操作中算法的框架,而将一些步骤延迟到子类中,模板方法使得子类可以不改变一个算法的结构,即可重定义该算法的某些步骤。

使用模板方法模式,可以将一个操作的复杂流程的实现步骤进行拆分,封装在一系列基本方法中,在抽象父类提供一个模板方法来定义整体执行步骤,而通过其子类来覆盖某个步骤,从而使得相同的执行步骤可以有不同的执行结果。

类结构如下:

图片图片

模板方法模式的优点在于:

  • 代码复用性高,父类的模板方法和具体方法都可以供多个子类复用。

  • 代码灵活性高,可根据业务迭代情况,灵活选择哪部分复用父类具体方法,哪部分进行子类覆盖实现。

嗯,这些底层源码的设计还是非常巧妙的,而设计模式本身也并不是有些人口中的过度设计的“花架子”。

ReentrantLock 与 AQS 

接下来我们看下,ReentrantLock 是如何通过 AQS 来实现锁机制的。

两者间的 UML 图如下所示:

图片图片

从图中可以看到,ReentrantLock 中有一个 Sync 内部类,Sync 继承自 AQS,且 Sync 有两个子类 FairSync 和 NonfairSync,分别用于实现公平锁和非公平锁。

我们梳理一下源码,看看 ReentrantLock 如何实现非公平锁的。

代码入口如下,我们看只有两个方法加上一个判断。

publicclass ReentrantLock implementsLock,java.io.Serializable{


    abstract static class Sync extends AbstractQueuedSynchronizer {
        final voidlock(){if(!initialTryLock())acquire(1);}
        }
    }     
}

来看下该方法的具体实现,简而言之,该方法尝试以独占的方式获取锁。

static final class NonfairSync extends Sync {    
    finalbooleaninitialTryLock(){        
        Threadcurrent=Thread.currentThread();if(compareAndSetState(0,1)){// first attempt is unguardedsetExclusiveOwnerThread(current);returntrue;}elseif(getExclusiveOwnerThread()==current){intc=getState()+1;if(c<0)// overflowthrow new Error("Maximum lock count exceeded");setState(c);returntrue;}elsereturnfalse;}
 }

先是通过 compareAndSetState(0, 1) 方法,以原子操作的方式将 AQS 类中的 state 变量值从 0 修改到 1。

我们在上文中提到过,state 变量来标识资源的锁占用状态,0 代表未占用,1 代表已占用,大于 1 则代表锁被重入,那么该操作就是尝试获取锁。

若该操作执行成功,则通过 setExclusiveOwnerThread(current) 作用是将当前线程设置为持有独占锁的线程,并返回 true,代表获取锁成功了。

再往下分析 getExclusiveOwnerThread() == current,这是判断当前线程是否已获取该锁且处于未释放的状态,若判断成立则将 state 变量+1代表重入,并返回 true 表示获取锁成功。

btw:从这段代码逻辑上看,知道为什么叫非公平锁了吧,一上来并没有通过 AQS 排队,而且先去争抢锁。

接下来我们继续来看acquire(1)方法,代码如下:

publicabstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable{publicfinal void acquire(intarg){if(!tryAcquire(arg))acquire(null,arg,false,false,false,0L);}
}

方法体重有两个方法加上一个判断,先来看 tryAcquire(arg) 方法的执行逻辑。

static final class NonfairSync extends Sync {
    protected finalbooleantryAcquire(intacquires){if(getState()==0&&compareAndSetState(0,acquires)){
            setExclusiveOwnerThread(Thread.currentThread());returntrue;}returnfalse;}
}

这块代码的逻辑竟然跟上面 initialTryLock() 方法的前半段几乎一样,先是通过 compareAndSetState(0, 1) 方法将 AQS 类中的 state 变量值从 0 修改到 1。

若该操作执行成功,则通过 setExclusiveOwnerThread(current) 作用是将当前线程设置为持有独占锁的线程,并返回 true,代表获取锁成功了。

btw:果然是非公平锁啊,这是誓要将插队争抢锁进行到底了。

下面就是 AQS 中的重头戏了,acquire(null, arg, false, false, false, 0L)方法,实现排队获取锁。

代码实现如下:

图片图片

这块代码并非主业务链路,先是进行了三个判断,当前节点不是 first 节点和 head 节点,且前驱结点不为null。

btw:head 节点可以理解为“当前持有独占锁的线程”,而在 head 节点之后的线程都处于阻塞、等待被唤醒的状态,而 first 节点则是同步队列中第一个等待获取锁的线程。

接下来 pred.status < 0 代表前驱节点已经被取消,结果为 true 则做一次等待队列清理。

而 pred.prev == null 则是判断前驱节点是否为 null,结果为 true 则跳到下一次循环中。

图片图片

这段代码的意思是,在当前节点为 first 节点或前驱节点为 null (未入队)的情况下,继续通过 tryAcquire(arg) 方法尝试获取锁。

图片图片

这段代码看起来比较复杂,其实也是有逻辑性的。

1、前两个大的逻辑分支判断的意思是,先创建一个独占节点,并将该节点加入到 CLH 队列的尾部。

2、如果当前节点为 first 节点,且自旋数大于 0,则继续尝试自旋获取锁。

3、将当前节点的状态值设置为“等待中”。

4、当前节点自旋失败,使用 LockSupport.pack() 方法挂起线程。

5、当独占锁被释放,队列中的 first 节点的线程将被唤醒,清除当前节点的等待状态。

21    2025-01-13 09:24:32    AQS Java JUC