面试官突击一问:你来讲讲AQS是什么吧?都是怎么用的

面试官突击一问:你来讲讲AQS是什么吧?都是怎么用的,第1张

因为AbstractQueuedSynchronizer是一个抽象类,他采用模板方法的设计模式,规定了独占和共享模式需要实现的方法,并且将一些通用的功能已经进行了实现,所以不同模式的使用方式,只需要自己定义好实现共享资源的获取与释放即可,至于具体线程在等待队列中的维护(获取资源入队列、唤醒出队列等),AQS已经实现好了。

所以根据共享资源的模式一般实现的方法有如下几个:

  • isHeldExclusively();// 是否为独占模式;但是只有使用到了Condition的,才需要去实现它。例如:ReentrantLock。

  • boolean tryAcquire(int arg); // 独占模式;尝试获取资源,成功返回true,失败返回false。

  • boolean tryRelease(int arg) ; // 独占模式;尝试释放资源,成功返回true,失败返回false。

  • int tryAcquireShared(int arg); // 共享模式;尝试获取资源,负数表示失败;0表示成功,但是没有剩余可用资源了;正数表示成功,且有剩余可用资源。

  • boolean tryReleaseShared(int arg) ; // 共享模式;尝试释放资源,若释放资源后允许唤醒后续等待节点返回true,否则返回false。

上面的这几个方法在AbstractQueuedSynchronizer这个抽象类中,都没有被定义为abstract的,说明这些方法都是可以按需实现的,共享模式下可以只实现共享模式的方法(例如CountDownLatch),独占模式下可以只实现独占模式的方法(例如ReentrantLock),也支持两种都实现,两种模式都使用(例如ReentrantReadWriteLock)。

AQS源码分析

我们先简单介绍AQS的两种模式的实现类的代表ReentrantLock(独占模式)和CountDownLatch(共享模式),是如何来共享资源的一个过程,然后再详细通过AQS的源码来分析整个实现过程。

  • ReentrantLock在初始化的时候state=0,表示资源未被锁定。当A线程执行lock()方法时,会调用tryAcquire()方法,将AQS中队列的模式设置为独占,并将独占线程设置为线程A,以及将state+1。 这样在线程A没有释放锁前,其他线程来竞争锁,调用tryAcquire()方法时都会失败,然后竞争锁失败的线程就会进入到队列中。当线程A调用执行unlock()方法将state=0后,其他线程才有机会获取锁(注意ReentrantLock是可重入的,同一线程多次获取锁时state值会进行垒加的,在释放锁时也要释放相应的次数才算完全释放了锁)。

  • CountDownLatch会将任务分成N个子线程去执行,state的初始值也是N(state与子线程数量一致)。N个子线程是并行执行的,每个子线程执行完成后countDown()一次,state会通过CAS方式减1。直到所有子线程执行完成后(state=0),会通过unpark()方法唤醒主线程,然后主线程就会从await()方法返回,继续后续 *** 作。

独占模式分析

在AbstractQueuedSynchronizer的类里面有一个静态内部类Node。它代表的是队列中的每一个节点。 其中Node节点有如下几个属性:

// 节点的状态
volatile int waitStatus;
// 当前节点的前一个节点
volatile Node prev;
// 当前节点的后一个节点
volatile Node next;
// 当前节点中所包含的线程对象
vola 《一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》无偿开源 威信搜索公众号【编程进阶路】 tile Thread thread;
// 等待队列中的下一个节点
Node nextWaiter;

每个属性代表的什么,已经写在代码注释中了。其中Node类中还有几个常量,代表了几个节点的状态(waitStatus)值。

/** waitStatus value to indicate thread has cancelled /
static final int CANCELLED = 1;
/
* waitStatus value to indicate successor’s thread needs unparking /
static final int SIGNAL = -1;
/
* waitStatus value to indicate thread is waiting on condition /
static final int CONDITION = -2;
/
*

  • waitStatus value to indicate the next acquireShared should
  • unconditionally propagate
    */
    static final int PROPAGATE = -3;

首先节点的状态值waitStatus默认是0,然后下面几个常量有自己具体的含义。 CANCELLED = 1; 代表的是当前节点从同步队列中取消,当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。 SIGNAL = -1; 代表后继节点处于等待状态。后继结点入队时,会将前继结点的状态更新为SIGNAL。 CONDITION = -2; 节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了 signal()方法后,该节点将会从等待队列中转移到同步队列中,加入到对同步状态的获取中。 PROPAGATE = -3; 表示在共享模式下,前继节点在释放资源后会唤醒后继节点,并将这种共享模式传播下去。

通过上面几个固定的常量值,我们可以看出节点状态中通常负数值通常表示节点处于有效的等待状态,而正数值代表节点已经被取消了。

所以AQS源码中有很多地方都用waitStatus>0或waitStatus<0这种方式来判断队列中节点的是否正常。

独占模式下,只能有一个线程占有锁资源,其他竞争资源的线程,在竞争失败后都会进入到等待队列中,等待占有锁资源的线程释放锁,然后再重新被唤醒竞争资源。

ReentrantLock加锁过程

ReentrantLock默认是非公平锁,就是说,线程在竞争锁的时候并不是按照先来后到的顺序来获取锁的,但是ReentrantLock也是支持公平锁的,在创建的时候传入一个参数值即可。 下面我们以ReentrantLock默认情况下的加锁来分析AQS的源码。 ReentrantLock并没有直接继承AQS类,而是通过内部类来继承AQS类的,这样自己的实现功能,自己用。 我们在用ReentrantLock加锁的时候都是调用的用lock()方法,那么我们来看看默认非公平锁下,lock()方法的源码:

/**

  • Sync object for non-fair locks
    */
    static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

/**

  • Performs lock. Try immediate barge, backing up to normal
  • acquire on failure.
    */
    final void lock() {
    if (compareAndSetState(0, 1))
    setExclusiveOwnerThread(Thread.currentThread());
    else
    acquire(1);
    }

protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}

通过源码可以看到,lock()方法,首先是通过CAS的方式抢占锁,如果抢占成功则将state的值设置为1。然后将对象独占线程设置为当前线程。

protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}

如果抢占锁失败,就会调用acquire()方法,这个acquire()方法的实现就是在AQS类中了,说明具体抢占锁失败后的逻辑,AQS已经规定好了模板。

public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

上面已经介绍了,独占模式是需要实现tryAcquire()方法的,这里首先就是通过tryAcquire()方法抢占锁,如果成功返回true,失败返回false。tryAcquire()方法的具体实现,是在ReentrantLock里面的,AQS类中默认是直接抛出异常的。

  • 首先获取state值,如果state值为0,说明无锁,那么通过CAS尝试加锁,成功后,将独占线程设置为当前线程。

  • 如果state值不为0,并且当前的独占线程和当前线程为同一线程,那么state重入次数加1。

  • 如果state值不为0,并且当前线程不是独占线程,直接返回false。

protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();// 获取state值
if (c == 0) {
// 如果state值为0,说明无锁,那么就通过cas方式,尝试加锁,成功后将独占线程设置为当前线程
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 如果是同一个线程再次来获取锁,那么就将state的值进行加1处理(可重入锁的,重入次数)。
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error(“Maximum lock count exceeded”);
setState(nextc);
return true;
}
return false;
}

我们继续来看acquire()方法,在执行完tryAcquire()方法后,如果加锁失败那么就会执行addWaiter()方法和acquireQueued(),这两个方法的作用是将竞争锁失败的线程放入到等待队列中。

addWaiter()方法的源码如下:

private Node addWaiter(Node mode) {
// 用参数指定的模式将当前线程封装成队列中的节点(EXCLUSIVE【独占】,SHARED【共享】)
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
// tail是队列的尾部节点,初始时队列为空,尾部节点为null,直接调用enq将节点插入队列
if (pred != null) {
// 将当前线程节点的前级节点指向队列的尾部节点。
node.prev = pred;
// 通过CAS方式将节点插入到队列中
if (compareAndSetTail(pred, node)) {
// 插入成功后,将原先的尾部节点的后级节点指向新的尾部节点
pred.next = node;
return node;
}
}
// 如果尾部节点为空或通过CAS插入队列失败则通过enq方法插入节点
enq(node);
return node;
}

addWaiter()中主要做了三件事:

  • 将当前线程封装成Node。

  • 判断队列中尾部节点是否为空,若不为空,则将当前线程的Node节点通过CAS插入到尾部。

  • 如果尾部节点为空或CAS插入失败则通过enq()方法插入到队列中。

那么enq()方法是又是怎么插入节点的呢?

enq()方法源码如下:

private Node enq(final Node node) {
// 看到死循环,就明白是通过自旋咯
for (;😉 {
// 当tail节点为空时直接将当前节点设置成尾部节点,并插入到队列中,以及设置它为head节点。
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 若是因为在addWaiter()方法中插入失败或第二次进入循环,那么将当前线程的前级节点指向尾部节点,并通过CAS方式将尾部节点指向当前线程的节点。
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}

其实enq()方法主要就是通过自旋将数据插入到队列中的 *** 作:

  • 当队列为空时,将当前节点设置为头节点和尾节点。

  • 进入二次循环后,将node添加到尾部。

这样addWaiter()方法就构造了一个队列,并将当前线程添加到了队列中了。 我们再回到acquire()方法中。

现在就剩下acquireQueued()方法没看了,这个方法中的 *** 作挺多的。

final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;😉 {
// 获取前级节点,如果未null,则抛出异常
final Node p = node.predecessor();
// 如果前级节点为head,并且执行抢占锁成功。
if (p == head && tryAcquire(arg)) {
// 抢占锁成功,当前节点成功新的head节点
setHead(node);
// 然后将原先的head节点指向null,方便垃圾回收进行回收
p.next = null; // help GC
failed = false;
return interrupted;
}
// 如果当前节点不为head,或者抢占锁失败。就根据节点的状态决定是否需要挂起线程。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed) // 如果获取锁异常,则出取消获取锁 *** 作。
cancelAcquire(node);
}
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}

  • 首先获取节点的前级节点。

  • 如果当前节点的前级节点是head那么就可以去抢占锁了。

  • 抢占成功后就将新节点设置为head,原先的head置为空。

  • 如果抢占锁失败,则根据waitStatus值决定是否挂起线程。

  • 最后,通过cancelAcquire()取消获取锁 *** 作。

下面看一下shouldParkAfterFailedAcquire()和parkAndCheckInterrupt()这两个方法是如何挂起线程的。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;// 获取前级节点
if (ws == Node.SIGNAL)// 如果前级节点的waitStatus值为SIGNAL(-1),说明当前节点也已经在等待唤醒了,直接返回true。
return true;
// 如果前级节点的waitStatus值大于0说明前级节点已经取消了。
if (ws > 0) {
// 如果前级节点已经是CANCEL状态了,那么会继续向前找,直到找到的节点不是CANCEL(waitStatue>0)状态的节点,然后将其设置为当前节点的前级节点。
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 如果前级节点为0或者其他不为-1的小于0的值,则将当前节点的前级节点设置为 SIGNAL(-1)
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}

parkAndCheckInterrupt()方法的作用就是挂起线程,如果shouldParkAfterFailedAcquire()方法成功,会执行parkAndCheckInterrupt()方法,它通过LockSupport的park()方法,将当前线程挂起(WAITING),它需要unpark()方法唤醒它,通过这样一种FIFO机制的等待,来实现Lock *** 作。

private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}

LockSupport是JDK从1.6开始提供的一个线程同步源语工具类,在这里主要用到了它的两个方法,挂起线程和唤醒线程:

public static void park() {
UNSAFE.park(false, 0L);
}
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}

LockSupport的挂起和唤醒线程都是不可重入的,它由一个许可标志,当调用park()时就会将许可设置为0,挂起线程,如果再调用一次park(),会阻塞线程。当调用unpark()时才会将许可标志设置成1。

ReentrantLock释放锁过程

ReentrantLock释放锁的过程主要有两个阶段:

  • 释放锁。

  • 唤醒挂起的线程。 unlock()方法的源码如下。

public void unlock() {
sync.release(1);
}

释放锁的方法是写在父类,AbstractQueuedSynchronizer类中的。 源码如下:

// 释放独占模式下的锁资源
public final boolean release(int arg) {
if (tryRelease(arg)) { // 尝试释放资源
Node h = head;
//释放成功后,判断头节点的状态是否为无锁状态,如果不为无锁状态就将头节点中的线程唤醒。
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false; // 释放资源失败,直接返回false
}

我们首先释放资源来看tryRelease()方法的源码,看看释放资源是怎样的过程。

protected final boolean tryRelease(int releases) {
// 从state中减去传入参数的相应值(一般为1)
int c = getState() - releases;
// 当释放资源的线程与独占锁现有线程不一致时,非法线程释放,直接抛出异常。
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 这里是处理重入锁的机制,因为可重入机制,所以每次都重入state值都加1,
//所以在释放的时候也要相应的减1,直到state的值为0才算完全的释放锁资源。
if (c == 0) {
free = true;
// 完全释放资源后,将独占线程设置为null,这样后面的竞争线程才有可能抢占。
setExclusiveOwnerThread(null);
}
// 重新赋值state
setState©;
return free;
}

tryRelease()方法在释放锁资源时,可以单纯的理解为是修改独占模式的状态值和置空占有线程的 *** 作。将state的值减掉相应的参数值(一般是1),如果计算结果为0,就将他的独占线程设置为null,其他线程才有机会抢占成功。 在加锁时,同一线程加一次锁,state状态值就会加1,在解锁的时候没解锁一次就会减1,同一个锁可重入,只有lock次数与unlock次数相同才会释放资源,将独占线程设置为null。

释放了资源后,我们再看唤醒挂起线程时的过程。这个过程就在unparkSuccessor()方法中。

private void unparkSuccessor(Node node) {
/* 获取当前节点的等待状态,一般是头节点,占有锁的节点是在头节点上。 /
int ws = node.waitStatus;
// 将当前节点的线程的状态值设为0,成为无锁状态。
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/

  • Thread to unpark is held in successor, which is normally
  • just the next node. But if cancelled or apparently null,
  • traverse backwards from tail to find the actual
  • non-cancelled successor.
    */
    Node s = node.next;// 获取下一个需要唤醒的节点线程。
    if (s == null || s.waitStatus > 0) {// 如果获取到的节点线程为空或已经取消
    s = null;
    // 就从队列的后面向前找,直到找到一个未取消的节点。
    for (Node t = tail; t != null && t != node; t = t.prev)
    if (t.waitStatus <= 0)
    s = t;
    }
    is held in successor, which is normally
  • just the next node. But if cancelled or apparently null,
  • traverse backwards from tail to find the actual
  • non-cancelled successor.
    */
    Node s = node.next;// 获取下一个需要唤醒的节点线程。
    if (s == null || s.waitStatus > 0) {// 如果获取到的节点线程为空或已经取消
    s = null;
    // 就从队列的后面向前找,直到找到一个未取消的节点。
    for (Node t = tail; t != null && t != node; t = t.prev)
    if (t.waitStatus <= 0)
    s = t;
    }

欢迎分享,转载请注明来源:内存溢出

原文地址: http://www.outofmemory.cn/langs/729535.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-27
下一篇 2022-04-27

发表评论

登录后才能评论

评论列表(0条)

保存