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

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

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

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

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

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

如下图所示:

图片图片

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

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

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

AQS 方法详述

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

独占模式:

// 获取锁
public final void acquire(int arg) 
// 以可中断的方式获取锁
public final void acquireInterruptibly(int arg)
// 以带超时时间的方式,尝试获取锁
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
// 释放锁
public final boolean release(int arg)

共享模式:

// 获取锁
public final void acquireShared(int arg)
// 以可中断的方式获取锁
public final void acquireSharedInterruptibly(int arg)
// 以带超时时间的方式,尝试获取锁
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
// 释放锁
public final boolean releaseShared(int arg)

需要子类实现的方法:

// 尝试获取独占锁
protected boolean tryAcquire(int arg);
// 尝试释放独占锁
protected boolean tryRelease(int arg);
// 尝试获取共享锁
protected int tryAcquireShared(int arg);
// 尝试释放共享锁
protected boolean tryReleaseShared(int arg);
// 判断当前线程是否正在持有锁
protected boolean isHeldExclusively();

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

是的,模板方法模式。

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

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

类结构如下:

图片图片

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

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

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

ReentrantLock 与 AQS 

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

两者间的 UML 图如下所示:

图片图片

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

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

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

public class ReentrantLock implements Lock, java.io.Serializable{


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

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

static final class NonfairSync extends Sync {    
    final boolean initialTryLock() {        
        Thread current = Thread.currentThread();        
        if (compareAndSetState(0, 1)) {
            // first attempt is unguarded            
            setExclusiveOwnerThread(current);            
            return true;        
        } else if (getExclusiveOwnerThread() == current) {
            int c = getState() + 1;            
            if (c < 0) // overflow                
                throw new Error("Maximum lock count exceeded");      
            setState(c);            
            return true;        
        } else            
            return false;   
     }
 }

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

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

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

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

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

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

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
    public final void acquire(int arg) {
        if (!tryAcquire(arg))
            acquire(null, arg, false, false, false, 0L);
    }
}

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

static final class NonfairSync extends Sync {
    protected final boolean tryAcquire(int acquires) {
        if (getState() == 0 && compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }
}

这块代码的逻辑竟然跟上面 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 节点的线程将被唤醒,清除当前节点的等待状态。

责任编辑:武晓燕 来源: 托尼学长
相关推荐

2021-08-09 07:47:40

Git面试版本

2021-11-25 10:18:42

RESTfulJava互联网

2022-03-21 09:05:18

volatileCPUJava

2024-10-24 16:14:43

数据传输CPU零拷贝

2020-12-01 08:47:36

Java异常开发

2024-09-27 15:43:52

零拷贝DMAIO

2020-06-12 15:50:56

options前端服务器

2020-06-19 15:32:56

HashMap面试代码

2024-08-27 12:36:33

2024-06-13 08:01:19

2021-09-16 07:52:18

算法应用场景

2024-09-26 16:01:52

2024-08-26 14:52:58

JavaScript循环机制

2019-07-26 06:42:28

PG架构数据库

2024-10-12 16:25:12

2019-05-10 10:50:04

Spring AOPJDK动态代理CGLIB动态代理

2021-09-26 15:34:21

内部结构方法区

2021-11-02 22:04:58

模式

2021-11-10 07:47:49

组合模式场景

2022-02-21 17:24:18

序列化对象存储
点赞
收藏

51CTO技术栈公众号