死磕JUC之AQS源码,一篇就够

AQS(抽象队列同步器)

    • 前言
    • 简单了解AQS的应用
      • 同步组件
      • JUC中的锁
    • AQS的结构
      • Node节点
      • AQS 队列结构图
    • 独占锁
      • acquire(获取锁)
        • 1、tryAcquire(尝试获取锁)
        • 2、addWaiter(添加等待节点)
        • 3、acquireQueued(排队获取锁)
        • 获取独占锁的流程图
        • 总结
      • release(释放锁)
        • 1、tryRelease(尝试释放锁)
        • 2、unparkSuccessor(唤醒后继节点)
    • 共享锁
      • acquireShared(获取同步状态)
        • 1、tryAcquireShared(尝试获取锁)
        • 2、doAcquireShared(自旋获取同步状态)
      • releaseShared(释放共享锁)
        • 1、tryReleaseShared(尝试释放共享锁)
        • 2、doReleaseShared(释放锁)
        • 3、unparkSuccessor(唤醒后继结点)

前言

什么是AQS?

AQS 全称是 AbstractQueuedSynchronizer(抽象队列同步器),顾名思义,是一个用来构建锁和同步器的框架,它底层用了 CAS 技术来保证操作的原子性,同时利用 FIFO 队列实现线程间的锁竞争,将基础的同步相关抽象细节放在 AQS,这也是 ReentrantLock、CountDownLatch 等同步工具实现同步的底层实现机制。

​ AQS 就是建立在 CAS 的基础之上,增加了大量的实现细节,例如获取同步状态、FIFO 同步队列,独占式锁和共享式锁的获取和释放等等,这些都是 AQS 类对于同步操作抽离出来的一些通用方法,这么做也是为了对实现的一个同步类屏蔽了大量的细节,大大降低了实现同步工具的工作量,这也是为什么 AQS 是其它许多同步类的基类的原因。

简单了解AQS的应用

同步组件

  • CountDownLatch(闭锁):允许一个或多个线程等待,直到在其他线程中执行的一组操作完成。
    • 核心方法:
      • await():线程会处于阻塞状态,直到计数器归零,才能继续执行
      • countDown():每次计数器减一
  • Semaphore(信号量):用于保证同一时间并发访问线程的数量,常用于有限访问资源的互斥使用或者并发限流等。
    • 核心方法:
      • acquire():获取一个许可,若已经满了,需要等待释放资源
      • release():释放一个许可,将当前信号量+1,然后唤醒等待的线程
  • CyclicBarrier(循环屏障):多个线程之间相互等待,只有每个线程都准备就绪后才能执行后续操作。当一个线程执行了await()方法,计数器会进行-1,待计数器达到0时,所有线程同时继续执行。当计数器被释放后,会调用reset()方法,计数器重新赋值,可以被重用,所以称为循环屏障。

JUC中的锁

  • ReentrantLock
  • ReentrantReadWriteLock(读写锁)

AQS的结构

​ 首先我们来看下AQS的几个重要的属性和方法:

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    /**
     * 等待队列的头节点,延迟初始化。
     * 除了初始化时,只能通过方法setHead进行修改。  Note:
     * 注意:如果head存在,那么它的 waitStatus 保证不会是 CANCELLED。
     */
    private transient volatile Node head;

    /**
     * 等待队列的尾节点,延迟初始化。
     */
    private transient volatile Node tail;

    /**
     * 同步状态
     * state > 0,为有锁状态,每次加锁在原有的基础上加一(可重入),反之减一
     * state = 0,为无锁状态
     */
    private volatile int state;
    
    /**
     * 采用CAS去更新state的值
     */
    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
    
}

可以理解为:head节点表示当前持有锁的节点,其余线程竞争锁失败后,会被加入到队尾。

​ AQS 提供了两种锁,分别是独占锁和共享锁,比如 ReentrantLock,它实现了独占锁的方法,而共享锁则指的是一个非独占操作,比如一些同步工具 CountDownLatch 和 Semaphore 等同步工具,下面是 AQS 对这两种锁提供的抽象方法。

独占锁

/**
 * 尝试去获取锁,获取成功返回true,否则返回false。
 */
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

/**
 * 尝试去释放同步状态,释放成功返回true,否则返回false。
 */
protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}

共享锁

/**
 * 尝试去获取共享锁,获取成功返回true,否则返回false。
 */
protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}

/**
 * 尝试去释放同步状态,释放成功返回true,否则返回false。
 */
protected boolean tryReleaseShared(int arg) {
    throw new UnsupportedOperationException();
}

​ 可以看到以上几个方法内部都会抛异常,所以该方法是需要继承AQS的子类自己去实现的,采用了模板方法设计模式。

什么是模板方法设计模式?

​ 模板模式:一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。这种类型的设计模式属于行为型模式。

Node节点

​ AQS里面有一个静态内部类Node,用于存储同步队列的节点:

static final class Node {
    
    /** 表示节点正在共享模式下等待的标记 */
    static final Node SHARED = new Node();
    
    /** 表示节点正在独占模式下等待的标记 */
    static final Node EXCLUSIVE = null;

    /** 取消状态,因超时或者中断,该状态的节点不会参与到竞争中,需要从等待队列中删除 */
    static final int CANCELLED =  1;
    /** 等待触发状态,如果当前节点释放了同步状态或者被取消,会通知后继节点,使其得以运行 */
    static final int SIGNAL    = -1;
    /** 
     * 等待条件状态,表示当前节点在等待 condition,即在 condition 队列中,
     * 当其他线程对condition调用了 signal 后,该节点将会从等待队列中进入
     * 同步队列中,此时状态设置为0
     */
    static final int CONDITION = -2;
    /** 状态需要向后传播,表示 releaseShared 需要被传播给后续节点,仅在共享锁模式下使用 */
    static final int PROPAGATE = -3;

    /** 等待状态:对于一般的同步节点,该字段初始化为0 */
    volatile int waitStatus;

    /** 前驱节点 */  
    volatile Node prev;

    /** 后继节点 */
    volatile Node next;

    /** 获取同步状态的线程 */
    volatile Thread thread;

    /** 下一个条件队列的等待节点 */
    Node nextWaiter;

    /** 如果节点在共享模式下等待,则返回true */
    final boolean isShared() {
        return nextWaiter == SHARED;
    }
    
    /** 返回上一个节点,如果为空,则抛出NullPointerException */
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

    Node() {    // Used to establish initial head or SHARED marker
    }

    Node(Thread thread, Node mode) {     // Used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
    }

    Node(Thread thread, int waitStatus) { // Used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
    
}

AQS 队列结构图

死磕JUC之AQS源码,一篇就够_第1张图片

独占锁

​ 独占锁就是如果有线程获取到锁,那么其他线程只能货锁失败,然后加入到等待队列的队尾等待被唤醒。

acquire(获取锁)

public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // 未获取到同步状态 && 线程的中断状态为 true 则中断当前线程
        // Thread.currentThread().interrupt();
        selfInterrupt();
}

源码解读

  1. 通过 tryAcquire(arg) 方法尝试去获取锁,但是这个方法需要子类去实现相应的逻辑;
  2. 获取锁成功后则不执行后面加入等待队列的逻辑,如果尝试获取锁失败后,则执行 addWaiter(Node.EXCLUSIVE) 方法将当前线程封装成一个 Node 节点,并加入到队列尾部;
  3. 封装成 Node 节点后,继续执行 acquireQueued 的逻辑,该方法主要是判断当前节点的前置节点是否是头节点,以尝试获取锁,如果成功获取锁,则将当前节点设置为头节点。

1、tryAcquire(尝试获取锁)

​ 尝试获取锁,获取成功返回true,失败返回false,该方法需要子类实现。

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

2、addWaiter(添加等待节点)

​ mode参数:Node.EXCLUSIVE(独占模式)、Node.SHARED(共享模式)

​ // 这里设置的是独占模式

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    // 判断队尾是否为空,不为空则直接将该节点加入到队尾
    if (pred != null) {
        node.prev = pred;
        // 采用CAS方式尝试将当前节点添加到尾节点
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 如果上面添加节点失败,这里会循环添加,直至成功
    enq(node);
    return node;
}

enq(自旋插入节点)

  1. 采用自旋操作,插入节点
  2. 如果队尾节点为空,则需要初始化队列,将头节点设置为空节点,头节点表示当前正在运行的节点
  3. 如果队尾节点不为空,则采用CAS自旋,将当前节点加入当队尾,直至成功为止
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;
            }
        }
    }
}

3、acquireQueued(排队获取锁)

​ 由于head节点表示当前持有锁的线程,如果当前节点的 prev 节点是 head 节点,有可能此时 head 节点已经释放锁,所以需要尝试获取锁。

final boolean acquireQueued(final Node node, int arg) {
    // 操作是否成功的标记
    boolean failed = true;
    try {
        // 是否中断的标记
        boolean interrupted = false;
        // 不断自旋
        for (;;) {
            // 当前节点的前置节点
            final Node p = node.predecessor();
            // 如果 prev 节点是头节点 && 获取到同步状态
            if (p == head && tryAcquire(arg)) {
                // 设置当前节点为头节点
                setHead(node);
                // 将前置节点从队列中移除
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 当prev不是头结点,或者获取锁失败
            // 判断当前线程是否需要阻塞 && 阻塞当前线程并且校验线程中断状态
            // 阻塞线程,避免线程不断获取锁,浪费系统资源
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            // 取消获取同步状态
            cancelAcquire(node);
    }
}

shouldParkAfterFailedAcquire

  1. 判断 pred 节点状态,如果为 SIGNAL 状态,则直接返回 true 执行挂起;
  2. 删除状态为 CANCELLED 的节点;
  3. 若 pred 节点状态为 0 或者 PROPAGATE,则将其设置为为 SIGNAL,返回 false,就会再从 acquireQueued 方法自旋操作重新判断。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 拿到当前节点的prev节点的等待状态
    int ws = pred.waitStatus;
	
    /**
     * 如果prev的waitStatus是signal,表示当prev释放了同步状态或者取消了,        
     * 会主动通知当前节点,所以当前节点可以安心的阻塞了
     */
    if (ws == Node.SIGNAL)
        return true;
    
    /**
     * 如果prev的waitStatuss > 0,表示为取消状态,需要将取消状态的节点从队列中移除,
     * 直到找到一个状态不是取消的节点为止,修改前置节点
     */
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 如果是其它状态,则操作CAS将其状态改为SIGNAL状态
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

其实就是根据 pred 节点状态来判断当前节点是否可以挂起,如果该方法返回 false,那么挂起条件还没准备好,就会重新进入 acquireQueued(final Node node, int arg) 的自旋体,重新进行判断。如果返回 true,那就说明当前线程可以进行挂起操作了,那么就会继续执行挂起。

parkAndCheckInterrupt(挂起逻辑)

​ LockSupport 提供 park() 和 unpark() 方法实现阻塞线程和解除线程阻塞。当调用 release 释放锁方法时,会调用 LockSupport.unPark 方法来唤醒后继节点。

private final boolean parkAndCheckInterrupt() {
    // 阻塞当前线程
    LockSupport.park(this);
    // 返回当前线程的中断状态
    return Thread.interrupted();
}

获取独占锁的流程图

死磕JUC之AQS源码,一篇就够_第2张图片

总结

​ 在AQS里面维护这一个FIFO的同步队列,当线程获取同步状态失败后,会加入到这个同步队列的队尾并保持自旋。在自旋时会判断其前驱节点是否为首节点,如果为首节点则不断尝试获取同步状态,获取成功则退出同步队列。如果前驱结点不是头结点或者获取锁失败的话,先将前驱节点状态设置成SIGNAL,然后调用LookSupport.park方法使得当前线程阻塞,避免一直自旋浪费系统资源。当线程执行完逻辑后,会释放同步状态,释放后会唤醒其后继节点。

release(释放锁)

public final boolean release(int arg) {
    // 尝试释放锁
    if (tryRelease(arg)) {
        Node h = head;
        // 判断 head 节点不为空 && head 节点状态不为 0
        // addWaiter 方法默认的节点状态为 0,此时该节点还没有挂起等待中的后继结点
        if (h != null && h.waitStatus != 0)
            // 唤醒后继节点
            unparkSuccessor(h);
        return true;
    }
    return false;
}

1、tryRelease(尝试释放锁)

​ 该方法也是需要继承AQS的子类进行实现,若同步状态释放成功返回true,失败返回false。

protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}

2、unparkSuccessor(唤醒后继节点)

​ 先从头结点的后继结点唤醒,如果后继结点不符合唤醒的条件,则从队尾一直往前找,找到符合唤醒条件的节点为止。

private void unparkSuccessor(Node node) {
    
    int ws = node.waitStatus;
    if (ws < 0)
        // 尝试清除头结点的状态,改为初始状态0
        compareAndSetWaitStatus(node, ws, 0);

    // 后继节点
    Node s = node.next;
    // 如果后继结点为空 或者 已经被取消
    if (s == null || s.waitStatus > 0) {
        s = null;
        // for循环从队列尾部一直往前找可以唤醒的节点
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        // 唤醒后继结点
        LockSupport.unpark(s.thread);
    
}

这里有个值得注意的点,为什么该节点的后继结点不满足唤醒条件时,需要从后往前遍历呢?

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);    
    Node pred = tail;
    // 重点看这里
    if (pred != null) {
        node.prev = pred; //step1
        if (compareAndSetTail(pred, node)) { //step2
            pred.next = node;  //step3
            return node;
        }
    }
    enq(node);
    return node;
}

​ 这时候,我们需要回到获取锁时,添加节点进同步队列的方法 addWaiter。如果你仔细观察这段代码, 可以发现节点入队不是一个原子操作, 也就是说,node.prev = pred; compareAndSetTail(pred, node) 这两个地方可以看作Tail入队的原子操作,但是此时pred.next = node;还没执行,如果这个时候执行了unparkSuccessor方法,就没办法从前往后找了,所以需要从后往前找。还有一点原因,在产生CANCELLED状态节点的时候,先断开的是Next指针,Prev指针并未断开,因此也是必须要从后往前遍历才能够遍历完全部的Node。

​ 综上所述,如果是从前往后找,由于极端情况下入队的非原子操作和CANCELLED节点产生过程中断开Next指针的操作,可能会导致无法遍历所有的节点。所以,唤醒对应的线程后,对应的线程就会继续往下执行。

共享锁

​ 共享式与独占式的最主要区别在于同一时刻独占式只能有一个线程获取同步状态,而共享式在同一时刻可以有多个线程获取同步状态。

acquireShared(获取同步状态)

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        // 获取失败,自旋获取同步状态
        doAcquireShared(arg);
}

1、tryAcquireShared(尝试获取锁)

​ 如果返回值大于等于0,则获取成功,否则获取失败,进入等待队列,等待获取资源。该方法由继承AQS的子类自己实现。

protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}

2、doAcquireShared(自旋获取同步状态)

private void doAcquireShared(int arg) {
    // 添加共享模式节点到队列中
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        // 自旋获取同步状态
        for (;;) {
            // 当前节点的前驱结点
            final Node p = node.predecessor();
            if (p == head) {
                //尝试获取同步状态
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    // 将当前节点设置为头结点,并且释放也是共享模式的后继节点
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

​ 这段代码逻辑几乎和独占式锁的获取一模一样,这里的自旋过程中能够退出的条件是当前节点的前驱节点是头结点并且tryAcquireShared(arg)返回值大于等于0即能成功获得同步状态

setHeadAndPropagate

propagate则代表tryAcquireShared的返回值,由于有if (r >= 0)的保证,propagate必定为>=0,这里返回值的意思是:如果>0,说明我这次获取共享锁成功后,还有剩余共享锁可以获取;如果=0,说明我这次获取共享锁成功后,没有共享锁可以获取。

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    setHead(node);

    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            // 真正的释放共享同步状态,并唤醒下一个节点
            doReleaseShared();
    }
}

releaseShared(释放共享锁)

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

1、tryReleaseShared(尝试释放共享锁)

protected boolean tryReleaseShared(int arg) {
    throw new UnsupportedOperationException();
}

2、doReleaseShared(释放锁)

private void doReleaseShared() {

    // 自旋释放共享同步状态
    for (;;) {
        Node h = head;
        // 如果头结点不为空 && 头结点不等于尾结点,说明存在有效的node节点
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                // 将头结点状态更新为0(初始值状态),因为此时头结点已经没用了
                // continue为了保证替换成功
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                // 唤醒后继节点
                unparkSuccessor(h);
            }
            // 如果状态为初始值状态0,那么设置成PROPAGATE状态
            // 确保在释放同步状态时能通知后继节点
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
    
}

3、unparkSuccessor(唤醒后继结点)

​ 跟上面独占锁的一样,这里就不贴源码了。

你可能感兴趣的:(多线程,java,并发编程)