您的位置 首页 java

面试官竟然问我Java的AQS锁实现原理,幸亏我看了底层源码

我们常见的并发锁 ReentrantLock CountDownLatch Semaphore CyclicBarrier 都是基于 AQS 实现的,所以说不懂 AQS 实现原理的,就不能说了解Java锁。

上篇文章讲了AQS的加锁流程,这篇文章再一块看一下AQS具体源码实现。

先回顾一下AQS的加锁流程

1. AQS加锁流程

面试官竟然问我Java的AQS锁实现原理,幸亏我看了底层源码

AQS 的加锁流程并不复杂,只要理解了 同步队列 条件队列 ,以及它们之间的数据流转,就算彻底理解了 AQS

  1. 当多个线程竞争AQS锁时,如果有个 线程 获取到锁,就把ower线程设置为自己
  2. 没有竞争到锁的线程,在同步队列中阻塞(同步队列采用双向链表,尾插法)。
  3. 持有锁的线程调用await方法,释放锁,追加到条件队列的末尾(条件队列采用单链表,尾插法)。
  4. 持有锁的线程调用 signal 方法,唤醒条件队列的头节点,并转移到同步队列的末尾。
  5. 同步队列的头节点优先获取到锁

了解AQS加锁流程之后,再去看源码就容易理解了。

2. AQS的数据结构

 // 继承自 abstract OwnableSynchronizer,为了记录哪个线程占用锁
public abstract class AbstractQueuedSynchronizer  extends  AbstractOwnableSynchronizer {
  
    // 同步状态,0表示无锁,每次加锁+1,释放锁-1
     private  volatile int  state ;

    // 同步队列的头尾节点
    private transient volatile Node head;
    private transient volatile Node tail;

    //  node 节点,用来包装线程,放到队列中
     static  final class Node {
        // 节点中的线程
        volatile Thread thread;

        // 节点状态
        volatile int waitStatus;

        // 同步队列的前驱节点和后继节点
        volatile Node prev;
        volatile Node next;

        // 条件队列的后继节点
        Node nextWaiter;
    }

    // 条件队列
    public class ConditionObject implements Condition {
        // 条件队列的头尾节点
        private transient Node firstWaiter;
        private transient Node lastWaiter;
    }
}  

首先AQS继承自AbstractOwnableSynchronizer,其实是为了记录哪个线程正在占用锁。

 public abstract class AbstractOwnableSynchronizer {

    private transient Thread exclusiveOwnerThread;

    // 设置占用锁的线程
    protected final  void  setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

    protected final Thread getExclusiveOwnerThread() {
        return exclusiveOwnerThread;
    }
}  

无论是同步队列还是条件队列中线程都需要包装成 Node 节点。

面试官竟然问我Java的AQS锁实现原理,幸亏我看了底层源码

虽然同步队列和条件队列都是由Node节点组成的,但是同步队列中是使用prev和next组成双向链表,nextWaiter只用来表示是共享模式还是排他模式。

条件队列没有使用到Node中prev和next属性,而是使用nextWaiter组成单链表。

这个复用对象的设计思想值得我们学习。

同步队列head节点是个哑节点,里面并没有存储线程对象。当然head节点也可以看成是给当前持有锁的线程使用的。

Node节点的状态(waitStatus)共有5种:

  • 1 cancelled:表示线程已经被取消
  • 0 初始化:Node节点的默认值
  • -1 signal: 表示节点线程在释放锁后要唤醒同步队列中的下一个节点线程
  • -2 condition: 当前节点在条件队列中
  • -3 propagate: 释放共享资源的时候会向后传播释放其他共享节点(用于共享模式)

3. AQS方法概览

AQS支持独占和共享两种访问资源的模式(独占模式又叫排他模式)。

独占模式的方法:

 // 加锁
acquire();
// 加可中断的锁
acquireInterruptibly();
// 一段时间内,加锁不成功,就不加了
tryAcquireNanos(int arg, long nanosTimeout);
// 释放锁
release();  

共享模式的方法:

 // 加锁
acquireShared();
// 加可中断的锁
acquireSharedInterruptibly();
// 一段时间内,加锁不成功,就不加了
tryAcquireSharedNanos(int arg, long nanosTimeout);
// 释放锁
releaseShared();  

独占模式和共享模式的方法并没有实现具体的加锁、释放锁逻辑,AQS中只是定义了加锁、释放锁的抽象方法。

留给子类实现的抽象方法:

 // 加独占锁
protected  boolean  tryAcquire(int arg) {
    throw new UnsupportedOperation Exception ();
}
// 释放独占锁
protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}

// 加共享锁
protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}
// 释放共享锁
protected boolean tryReleaseShared(int arg) {
    throw new UnsupportedOperationException();
}

// 判断是否是当前线程正在持有锁
protected boolean isHeldExclusively() {
    throw new UnsupportedOperationException();
}  

这里就用到了设计模式中的模板模式,父类AQS定义了加锁、释放锁的流程,子类 ReentrantLock CountDownLatch Semaphore CyclicBarrier 负责实现具体的加锁、释放锁逻辑。

这不是个面试知识点吗?

面试官再问你,你看过哪些框架源码使用到了设计模式?

你就可以回答AQS源码中用到了模板模式,巴拉巴拉,妥妥的加分项!

面试官竟然问我Java的AQS锁实现原理,幸亏我看了底层源码

4. AQS源码剖析

整个加锁流程如下:

面试官竟然问我Java的AQS锁实现原理,幸亏我看了底层源码

先看一下加锁方法的源码:

4.1 加锁

 // 加锁方法,传参是1
public final void acquire(int arg) {
    // 1. 首先尝试获取锁,如果获取成功,则设置state+1,exclusiveOwnerThread=currentThread(留给子类实现)
     if  (!tryAcquire(arg) &&
            // 2. 如果没有获取成功,把线程组装成Node节点,追加到同步队列末尾
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
        // 3. 加入同步队列后,将自己挂起
        selfInterrupt();
    }
}  

再看一下addWaiter方法源码,作用就是把线程组装成Node节点,追加到同步队列末尾。

 // 追加到同步队列末尾,传参是共享模式or排他模式
private Node addWaiter(Node mode) {
    // 1. 组装成Node节点
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        // 2. 在 多线程 竞争不激烈的情况下,通过CAS方法追加到同步队列末尾
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 3. 在多线程竞争激烈的情况下,使用死循环保证追加到同步队列末尾
    enq(node);
    return node;
}

// 创建Node节点,传参是线程,共享模式or排他模式
Node(Thread thread, Node mode) {
    this.thread = thread;
    this.nextWaiter = mode;
}

// 通过死循环的方式,追加到同步队列末尾
private Node enq(final Node node) {
    for (; ; ) {
        Node t =  tail ;
        if (t == null) {
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}  

再看一下addWaiter方法外层的acquireQueued方法,作用就是:

  1. 在追加到同步队列末尾后,再判断一下前驱节点是不是头节点。如果是,说明是第一个加入同步队列的,就再去尝试获取锁。
  2. 如果获取锁成功,就把自己设置成头节点。
  3. 如果前驱节点不是头节点,或者获取锁失败,就逆序遍历同步队列,找到可以将自己唤醒的节点。
  4. 最后才放心地将自己挂起
 // 追加到同步队列末尾后,再次尝试获取锁
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (; ; ) {
            // 1. 找到前驱节点
            final Node p = node.predecessor();
            // 2. 如果前驱节点是头结点,就再次尝试获取锁
            if (p == head && tryAcquire(arg)) {
                // 3. 获取锁成功后,把自己设置为头节点
                setHead(node);
                p.next = null;
                failed = false;
                return  interrupt ed;
            }
            // 4. 如果还是没有获取到锁,找到可以将自己唤醒的节点
            if (shouldParkAfterFailedAcquire(p, node) &&
                    // 5. 最后才放心地将自己挂起
                    parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}  

再看一下shouldParkAfterFailedAcquire方法,是怎么找到将自己唤醒的节点的?为什么要找这个节点?

 // 加入同步队列后,找到能将自己唤醒的节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    // 1. 如果前驱节点的状态已经是SIGNAL状态(释放锁后,需要唤醒后继节点),就无需操作了
    if (ws == Node.SIGNAL)
        return true;
    // 2. 如果前驱节点的状态是已取消,就继续向前遍历
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 3. 找到了不是取消状态的节点,把该节点状态设置成SIGNAL
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}  

从代码中可以很清楚的看到,目的就是为了找到不是取消状态的节点,并把该节点的状态设置成SIGNAL。

状态是SIGNAL的节点,释放锁后,需要唤醒其后继节点。

简单理解就是:小弟初来乍到,特意来知会老大一声,有好事,多通知小弟。

再看一下释放锁的逻辑。

4.2 释放锁

释放锁的流程如下:

面试官竟然问我Java的AQS锁实现原理,幸亏我看了底层源码

释放锁的代码逻辑比较简单:

 // 释放锁
public final boolean release(int arg) {
    // 1. 先尝试释放锁,如果时候成功,则设置state-1,exclusiveOwnerThread=null(由子类实现)
    if (tryRelease(arg)) {
        Node h = head;
        // 2. 如果同步队列中还有其他节点,就唤醒下一个节点
        if (h != null && h.waitStatus != 0)
            // 3. 唤醒其后继节点
            unparkSuccessor(h);
        return true;
    }
    return false;
}  

再看一下唤醒后继节点的方法

 // 唤醒后继节点
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    // 1. 如果头节点不是取消状态,就重置成初始状态
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;
    // 2. 如果后继节点是null或者是取消状态
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 3. 从队尾开始遍历,找到一个有效状态的节点
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    // 3. 唤醒这个有效节点
    if (s != null)
         Lock Support.unpark(s.thread);
}  

4.3 await等待

await等待的流程:

面试官竟然问我Java的AQS锁实现原理,幸亏我看了底层源码

持有锁的线程可以调用await方法,作用是:释放锁,并追加到条件队列末尾。

 // 等待方法
public final void await() throws InterruptedException {
    // 如果线程已中断,则中断
    if (Thread.interrupted())
        throw new InterruptedException();
    // 1. 追加到条件队列末尾
    Node node = addConditionWaiter();
    // 2. 释放锁
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    // 3. 有可能刚加入条件队列就被转移到同步队列了,如果还在条件队列,就可以放心地挂起自己
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // 4. 如果已经转移到同步队列,就尝试获取锁
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null)
        // 5. 清除条件队列中已取消的节点
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}  

再看一下addConditionWaiter方法,是怎么追加到条件队列末尾的?

 // 追加到条件队列末尾
private Node addConditionWaiter() {
    Node t = lastWaiter;
    // 1. 清除已取消的节点,找到有效节点
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    // 2. 创建Node节点,状态是-2(表示处于条件队列)
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    // 3. 追加到条件队列末尾
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}  

4.4 signal唤醒

signal唤醒的流程:

面试官竟然问我Java的AQS锁实现原理,幸亏我看了底层源码

唤醒条件队列的头节点,并追加到同步队列末尾。

 // 唤醒条件队列的头节点
public final void signal() {
    // 1. 只有持有锁的线程才能调用signal方法
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 2. 找到条件队列的头节点
    Node first = firstWaiter;
    if (first != null)
        // 3. 开始唤醒
        doSignal(first);
}

// 实际的唤醒方法
private void doSignal(Node first) {
    do {
        // 4. 从条件队列中移除头节点
        if ((firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
        // 5. 使用死循环,一定要转移一个节点到同步队列
    } while (!transferForSignal(first) &&
            (first = firstWaiter) != null);
}  

到底是怎么转移到同步队列末尾的?

 // 实际转移方法
final boolean transferForSignal(Node node) {
    // 1. 把节点状态从CONDITION改成0
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    // 2. 使用死循环的方式,追加到同步队列末尾(前面已经讲过)
    Node p = enq(node);
    int ws = p.waitStatus;
    // 3. 把前驱节点状态设置SIGNAL(通知他,别忘了唤醒老弟)
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}  

5. 总结

看完整个AQS的源码,是不是完全理解了AQS加锁、释放锁、以及同步队列和条件队列数据流转的逻辑了。

面试官竟然问我Java的AQS锁实现原理,幸亏我看了底层源码

连AQS这么复杂的源码你都搞清楚了,下篇带你一块学习ReentrantLock源码,应该就轻松多了。

文章来源:智云一二三科技

文章标题:面试官竟然问我Java的AQS锁实现原理,幸亏我看了底层源码

文章地址:https://www.zhihuclub.com/195302.shtml

关于作者: 智云科技

热门文章

网站地图