多线程AQS

153 阅读8分钟

ReentrantLock

ReentrantLock的实现依赖于AQS。
首先 ReentrantLock 中有一个内部类sync,sync继承了AQS并实现了里面方法(看源码感觉更多的是调用),然后又有公平锁和非公平锁共同继承了sync来实现方法。其他方法的调用就是创建公平/非公平锁对象再继续实现,所以AQS是ReentrantLock的基础。 sync定义

abstract static class Sync extends AbstractQueuedSynchronizer 

非公平锁

static final class NonfairSync extends Sync 

公平锁

static final class FairSync extends Sync 

AQS

是什么

AQSAbstractQueuedSynchronizer的简称,即抽象队列同步器,从字面意思上理解:

  • 抽象:抽象类,只实现一些主要逻辑,有些方法由子类实现;
  • 队列:使用先进先出(FIFO)队列存储数据;
  • 同步:实现了同步的功能。

有什么用

AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的同步器,比如我们提到的ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。 我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器,只要之类实现它的几个protected方法就可以了。

结构及概念

AQS核心思想是,如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中。CLH:Craig、Landin and Hagersten队列,是单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO),AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。

image.png

而AQS类本身实现的是一些排队和阻塞的机制,比如具体线程等待队列的维护(如获取资源失败入队/唤醒出队等)。它内部使用了一个先进先出(FIFO)的双端队列,并使用了两个指针head和tail用于标识队列的头部和尾部。

节点

节点/node 是AQS中的基础,线程也是存储在节点中的。

static final class Node {
    // 标记一个结点(对应的线程)在共享模式下等待
    static final Node SHARED = new Node();
    // 标记一个结点(对应的线程)在独占模式下等待
    static final Node EXCLUSIVE = null; 

    // waitStatus的值,表示该结点(对应的线程)已被取消
    static final int CANCELLED = 1; 
    // waitStatus的值,表示后继结点(对应的线程)需要被唤醒
    static final int SIGNAL = -1;
    // waitStatus的值,表示该结点(对应的线程)在等待某一条件
    static final int CONDITION = -2;
    /*waitStatus的值,表示有资源可用,新head结点需要继续唤醒后继结点(共享模式下,多线程并发释放资源,而head唤醒其后继结点后,需要把多出来的资源留给后面的结点;设置新的head结点时,会继续唤醒其后继结点)*/
    static final int PROPAGATE = -3;

    // 等待状态,取值范围,-3,-2,-1,0,1
    volatile int waitStatus;
    volatile Node prev; // 前驱结点
    volatile Node next; // 后继结点
    volatile Thread thread; // 结点对应的线程
    Node nextWaiter; // 等待队列里下一个等待条件的结点


    // 判断共享模式的方法
    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    Node(Thread thread, Node mode) {     // Used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
    }
 //返回前驱节点,没有则返回异常
final Node predecessor() throws NullPointerException {
    Node p = prev;
    if (p == null)
        throw new NullPointerException();
    else
        return p;
}
 
    // 其它方法忽略,可以参考具体的源码
}

// AQS里面的addWaiter私有方法,不在node结构中
private Node addWaiter(Node mode) {
    // 使用了Node的这个构造函数
    Node node = new Node(Thread.currentThread(), mode);
    // 其它代码省略
}

资源有两种共享模式,或者说两种同步方式/两种锁:

  • 独占模式(Exclusive):资源是独占的,一次只能一个线程获取。如ReentrantLock。
  • 共享模式(Share):同时可以被多个线程获取,具体的资源个数可以通过参数指定。如Semaphore/CountDownLatch。

实现

AQS内部使用了一个volatile的变量state来作为资源的标识。同时定义了几个获取和改版state的protected方法,子类可以覆盖这些方法来实现自己的逻辑:

private volatile int state;
//获取state
protected final int getState()
//设置state
protected final void setState(int newState)
//通过cas操作设置```
protected final boolean compareAndSetState(int expect, int update)
//都是原子操作

上面的三种方法都由final修饰,这说明子类中无法重写它们。
我们通过修改state的值来实现共享模式和独占模式的实现。
独占:state初始设置为0,获得锁则为1。尝试获取锁就是判断是否为0。 共享:state初始设置为某值,获取锁时判断是否为0,大于0通过cas自减,为0则说明不能再共享了。

image.png

image.png

源码

AQS的设计是基于模板方法模式的,前面提到了,我们自己也可以通过继承AQS实现我们自己的同步器,子类要去实现一部分方法,主要有:

image.png 上面的方法都被protected修饰,在AQS中使用都是抛出异常

protected boolean isHeldExclusively() {
    throw new UnsupportedOperationException();
}

下面通过获取资源和释放资源两部分来说明一些源代码
ReentrantLock中非公平锁中的加锁操作:

final void lock() {
//设置state成功,将当前线程设置为资源拥有线程
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
    //否则执行下面方法,调用的AQS的方法
        acquire(1);
}

在AQS中acquire是这样的

public final void acquire(int arg) {
//先调用TryAcquire,失败就继续执行acquireQueued中的addWaiter方法,成功后执行selfInterrupt方法
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

tryAcquire在AQS中是抛出异常,所以该方法是在子类中定义好的,是使用的ReentrantLock的方法。这里就不说了。
如果tryAcquire成功则说明资源获取成功,后续不再执行。
失败就要先执行addWaiter方法,从名字就知道是将线程添加到等待队列(调用的AQS中的方法,RL中没有重写该方法)addWaiter是一个在双端链表添加尾节点的操作,需要注意的是,双端链表的头结点是一个无参构造函数的头结点。

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;
        
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

(1)通过当前的线程和锁模式新建一个节点。
(2)Pred指针指向尾节点Tail。
(3)将New中Node的Prev指针指向Pred。
(4)通过compareAndSetTail方法,完成尾节点的设置。这个方法主要是对tailOffset和Expect进行比较,如果tailOffset的Node和Expect的Node地址是相同的,那么设置Tail的值为Update的值。
(5) 如果Pred指针是Null(说明等待队列中没有元素),或者当前Pred指针和Tail指向的位置不同(说明被别的线程已经修改)。
就是说cas操作成功就返回节点,失败就向后执行。

自旋CAS插入等待队列
private Node enq(final Node node) {
    for (;;) {
    //获取头结点
        Node t = tail;
        //没有头结点就创建一个
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

回到acquireQueued方法,其源码如下:

final boolean acquireQueued(final Node node, int arg) {  
    // 标记是否成功拿到资源  
    boolean failed = true;  
    try {  
        // 标记等待过程中是否中断过  
        boolean interrupted = false;  
        // 开始自旋,要么获取锁,要么中断  
        for (;;) {  
            // 获取当前节点的前驱节点  
            final Node p = node.predecessor();  
            // 如果p是头结点,说明当前节点在真实数据队列的首部,就尝试获取锁(别忘了头结点是虚节点)  
            if (p == head && tryAcquire(arg)) {  
                // 获取锁成功,头指针移动到当前node  
                setHead(node);  
                p.next = null; // help GC  
                failed = false;  
                return interrupted;  
            }  
            // 说明p为头节点且当前没有获取到锁(可能是非公平锁被抢占了)或者是p不为头结点,这个时候就要判断当前node是否要被阻塞(被阻塞条件:前驱节点的waitStatus为-1),防止无限循环浪费资源。具体两个方法下面细细分析  
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())  
                interrupted = true;  
        }  
    } finally {  
        if (failed)  
            cancelAcquire(node);  
    }  
}

流程图如下: image.png

释放资源/锁 ReentrantLock在解锁的时候,并不区分公平锁和非公平锁。

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

AQS中的relase

public final boolean release(int arg) {  
    // 上边自定义的tryRelease如果返回true,说明该锁没有被任何线程持有  
    //tryrelase由reentrantLock实现
    if (tryRelease(arg)) {  
        // 获取头结点  
        Node h = head;  
        // 头结点不为空并且头结点的waitStatus不是初始化节点情况,解除线程挂起状态  
        if (h != null && h.waitStatus != 0)  
            unparkSuccessor(h);  
        return true;  
    }  
    return false;  
}
private void unparkSuccessor(Node node) {

// 如果状态是负数,尝试把它设置为0

int ws = node.waitStatus;

if (ws < 0)

compareAndSetWaitStatus(node, ws, 0);

// 得到头结点的后继结点head.next

Node s = node.next;

// 如果这个后继结点为空或者状态大于0

// 通过前面的定义我们知道,大于0只有一种可能,就是这个结点已被取消

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;

}

// 如果后继结点不为空,

if (s != null)

LockSupport.unpark(s.thread);

}