Java并发工具包中的AbstractQueuedSynchronizer类浅析

版权声明:转载请注明出处 https://blog.csdn.net/abc123lzf/article/details/82532036

一、引言

AbstractQueuedSynchronizer(以下简称AQS)是Java并发工具包中用于构建锁或其它同步工具的基础框架,其ReentrantLock和Semaphore都是基于这个框架实现的。它的内部维护了一个FIFO队列,在有很多线程竞争资源时,线程对象会被加入到此队列等待。AQS的父类是AbstractOwnableSynchronizer,这个父类比较简单,仅有一个Thread类型的成员变量和它的get、set方法(protected权限),代表一个竞争到资源的线程(一般用于独占资源模式而不是共享资源模式)。

Java并发包有很多工具类采用了这个框架:ReentrantLock、Semaphore等,所以了解它的实现原理是十分必要的。


二、使用方法

AQS作为一个同步框架,提供了5种非final的protected方法用于同步器实现自己的功能:

//尝试获取独占式资源
protected boolean tryAcquire(int arg);
//尝试释放独占式资源
protected boolean tryRelease(int arg);
//尝试获得共享式资源
protected int tryAcquireShared(int arg);
//尝试释放共享式资源
protected int tryReleaseShared(int arg);
//返回当前线程是否获得了该资源
protected boolean isHeldExclusively();

int参数一般用于调整state变量。

这些方法由AQS的public方法负责调用,并且在AQS的默认实现都是抛出UnsupportedOperationException异常,需要由子类根据自己的实际业务需求自定义获取资源的策略。比如ReentrantLock内部类Sync就重写了tryAcquire、tryRelease和isHeldExclusively方法,Semaphore作为共享资源同步器,其内部类Sync自然就重写了tryAcquireShared、tryReleaseShared方法。


三、原理分析

1
AQS内部采用了一个非阻塞式队列(由CAS算法保证线程安全性),一般拥有一个头结点和多个包含等待获取资源的线程对象结点,这些结点有序地排列着,等待着资源的释放然后获取并占有它。

AQS使用了一个int型的变量state代表资源的状态。比如在ReentrantLock中,state为0时代表没有线程持有锁,state为1时代表有线程持有了锁。对于尝试获得锁的线程而言,会通过CAS算法将其state由0设为1,代表取得了该锁。

1、Node结点

AQS通过一个内部类Node来代表一个队列的元素。

static final class Node {
    static final Node SHARED = new Node();
    static final Node EXCLUSIVE = null;

    //当这个Node对应的线程等待超时或被中断则为这个状态
    static final int CANCELLED =  1;
    //后继的节点处于等待状态,当前节点的线程如果释放了资源或者被取消
    //将会通知后继节点,使后继节点的线程得以运行
    static final int SIGNAL    = -1;
    //节点处于等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了
    //signal()方法后,该节点从等待队列中转移到同步队列中,加入到对同步状态的获取中
    static final int CONDITION = -2;
    //下一次的共享状态会被无条件的传播下去(用于共享资源的同步器)
    static final int PROPAGATE = -3;
    //Node状态,对应上面几种,默认为0
    volatile int waitStatus;
    //后驱结点
    volatile Node prev;
    //前驱结点
    volatile Node next;
    //等待的线程
    volatile Thread thread;
    //等待节点的后继节点,为SHARED或EXCLUSIVE
    Node nextWaiter;
}

Node实例保存了维持队列的前驱引用和后驱引用,并保存了等待线程的对象和资源模式。
先记住上述Node状态所对应的情况,Node默认状态为0。

2、acquire方法

我们从acquire方法开始分析AQS的原理:

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

acquire方法表示一个线程尝试获取线程独占式资源。

方法执行步骤如下:
1、首先调用tryAcquire尝试获取资源(由子类自己实现获取资源的策略),如果获取成功,该方法结束。
2、如果没有获取成功,则首先调用addWaiter方法将当前线程添加到AQS实现的队列中,然后调用acquireQueued方法等待获取资源。如果acquireQueued方法返回false,则该线程是正常获取到资源的,如果为true,则该线程被打断(interrupt),则调用selfInterrupt彻底终止当前线程。

addWaiter方法,mode表示资源模式(只有两种:Node.EXCLUSIVE表示独占资源,Node.SHARED表示共享资源):

private Node addWaiter(Node mode) {
    //将当前线程包装为一个Node结点
    Node node = new Node(Thread.currentThread(), mode);
    //将尾结点引用赋给pred
    Node pred = tail;
    //如果尾结点不为null
    if (pred != null) {
        //将新Node的前驱指针指向尾结点
        node.prev = pred;
        //利用CAS算法安全地将这个新Node添加到队列尾部
        if (compareAndSetTail(pred, node)) {
            //将旧尾结点的后驱指针指向新Node
            pred.next = node;
            return node;
        }
    }
    //如果尾结点为null或尝试通过CAS将新Node设为尾结点没有成功,
    //则采用enq方法自旋地将新Node添加到队列尾部
    enq(node);
    return node;
}

private Node enq(final Node node) {
    //反复循环直至添加成功为止
    for (;;) {
        //将尾结点赋值给t
        Node t = tail;
        //如果队列为空
        if (t == null) {
            //通过CAS将一个空Node作为头结点
            if (compareAndSetHead(new Node()))
                //将空Node也作为尾结点
                tail = head;
        } else {
            //将新Node的前驱指针指向尾结点
            node.prev = t;
            //通过CAS将新Node作为尾结点,若失败则重新开始循环
            if (compareAndSetTail(t, node)) {
                //尾结点的后驱指针指向新Node
                t.next = node;
                //返回旧的尾结点
                return t;
            }
        }
    }
}

addWaiter总是能够保证以线程安全且非阻塞的方式将结点添加到队列尾部。

添加到队列以后,再通过acquireQueued方法尝试获取资源:

final boolean acquireQueued(final Node node, int arg) {
    //是否成功获取到资源
    boolean failed = true;
    try {
        //该node对应的线程是否被打断
        boolean interrupted = false;
        for (;;) {
            //获得新Node的排在前面的Node
            final Node p = node.predecessor();
            //如果排在前面的是头结点,那么这个Node就是第队列头部的借点
            //这时,这个Node就有资格尝试获取资源了(尝试调用tryAcquire)
            if (p == head && tryAcquire(arg)) {
                //将此node设为头结点(会将前驱后驱指针设置为null)
                setHead(node);
                //删除这个结点,代表出队
                p.next = null; 
                //成功拿到资源
                failed = false;
                //返回该线程是否打断
                return interrupted;
            }
            //使当前线程进入等待状态直到该方法返回代表等待结束或被打断
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                //如果等待过程中该线程被中断,则标记为true
                interrupted = true;
        }
    } finally {
        //如果线程等待超时或被中断则将这个Node设置为CANCELLED状态
        if (failed)
            cancelAcquire(node);
    }
}

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    //获取前面的Node的等待状态
    int ws = pred.waitStatus;
    //如果前面的结点获取到资源时可以通知本结点,那么返回true,执行parkAndCheckInterrupt
    if (ws == Node.SIGNAL)
        return true;
    //如果前面的Node为CANCELLED状态
    if (ws > 0) {
        //一直往队列前面找,直到找到正常状态的Node,处于CANCELLED状态的Node在循环结束后会失去引用
        do {
            //往队列前面找
            pred = pred.prev;
            //将node的前驱指针指向pred(意味着忽略掉CANCELLED状态的Node,这种状态的Node失去了引用会被GC)
            node.prev = pred;
        } while (pred.waitStatus > 0);
        //将正常状态的Node的后驱指针指向node
        pred.next = node;
    } else {
        //通过CAS将状态设为SIGNAL
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

private final boolean parkAndCheckInterrupt() {
    //调用park使线程进入等待状态,这里会被阻塞直到等到前面的Node通知或被interrupt
    LockSupport.park(this);
    //返回线程有没有被中断
    return Thread.interrupted();
}

Node.SIGNAL状态表示了这个结点对应的资源释放资源后会主动通知队列后面的线程。
shouldParkAfterFailedAcquire从方法名可以看出,该方法的作用是当获取资源失败时是否应该使该线程等待。

acquireQueued方法的执行流程如下:
1、首先判断该Node是否在队列最前端(不包括头结点),如果不是跳到步骤2,如果是则调用tryAcquire尝试获取资源,成功获取到资源后将此Node出队,方法结束。如果没有成功获取到资源,跳转到步骤2
2、调用shouldParkAfterFailedAcquire方法,如果该Node前面的Node处于SIGNAL状态则返回true进入步骤3。否则如果前面的Node处于CANCELLED状态,则循环删除CANCELLED状态的结点,返回false,跳转到步骤1。如果前面的Node状态正常,则通过CAS将前面的Node设为SIGNAL状态,跳转到步骤1
3、阻塞该线程直到资源释放后得到通知或线程被中断,跳转到步骤1。
用流程图表示acquireQueued状态:
这里写图片描述

看完后,我们来回顾下acquire方法执行流程:
这里写图片描述

3、release方法

release方法用于释放独占资源

public final boolean release(int arg) {
    //调用tryRelase释放资源,如果没有成功方法结束并返回false
    if (tryRelease(arg)) {
        //获取队列头节点
        Node h = head;
        //如果头节点不为null且不为默认0
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

private void unparkSuccessor(Node node) {
    //获取头结点状态
    int ws = node.waitStatus;
    //若状态不为CANCELLED或0则通过CAS将其状态修改为0
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    //将node后面的结点赋给s
    Node s = node.next;
    //如果后面的Node状态为CANCELLED
    if (s == null || s.waitStatus > 0) {
        //删除这个结点
        s = null;
        //从队列尾部搜索,删除CANCELLED状态的结点
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    //激活正在等待资源的线程
    if (s != null)
        LockSupport.unpark(s.thread);
}

release操作相对于acquire方法还是简单很多,执行步骤如下:
1、首先尝试调用tryRelease方法尝试释放资源,如果释放失败方法直接返回。如果释放成功,执行步骤2
2、获取头结点,如果头结点为null或头结点状态为0,则说明没有线程在队列中等待,方法返回true,资源释放成功。否则,执行步骤3
3、获取头结点的状态,如果状态正常(表明有线程正在等待)则将这个头结点状态设为0,如果头结点中后面的结点有处于CANCELLED状态的,则从队列后面依次移除这些节点直到遇到正常状态的结点。然后,激活那个队列最前端在等待的线程获取资源。

4、acquireShared

线程调用acquireShared代表需要获取共享资源。

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

如果tryAcquireShared方法返回值小于0则获取资源失败,调用doAcquireShared将线程加入队列。

private void doAcquireShared(int arg) {
    //同样和acquire方法类似,将线程包装为Node加入到队列,只不过模式为SHARED
    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);
                //r大于等于0代表获取资源成功
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null;
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

//该方法将头结点设为node,如果还有剩余资源可用再唤醒后面的线程
private void setHeadAndPropagate(Node node, int propagate) {
    //获取头结点
    Node h = head; 
    //将node设为新的头结点
    setHead(node);
    //如果还有剩余的资源
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
        //获取node的后驱结点
        Node s = node.next;
        //如果后驱结点为空或者后驱结点是共享模式的
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        //如果头结点不为空并且队列不为空
        if (h != null && h != tail) {
            //获取头结点状态
            int ws = h.waitStatus;
            //如果处于SIGNAL状态
            if (ws == Node.SIGNAL) {
                //通过CAS将SIGNAL状态修改为0,如果修改失败重新开始循环
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;
                //唤醒后面的节点
                unparkSuccessor(h);
            //如果状态为0那么通过CAS将0状态修改为PROPAGATE状态,修改失败重新开始循环
            } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;
        }
        //如果头结点没有被其它线程更改,方法结束
        if (h == head)
            break;
    }
}

acquireShared方法逻辑上大体和acquire方法差不多,只是体现在setHead方法上的不同。

5、releaseShared方法

releaseShared方法用于释放共享资源

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

和release方法类似,该方法首先会调用tryReleaseShared方法尝试释放资源,如果释放失败方法结束,直接返回false。如果成功释放了资源则调用AQS内部方法doReleaseShared方法。

private void doReleaseShared() {
    for (;;) {
        //获取头结点
        Node h = head;
        //如果队列中存在等待的线程
        if (h != null && h != tail) {
            //获取头结点状态
            int ws = h.waitStatus;
            //如果状态为SIGNAL
            if (ws == Node.SIGNAL) {
                //通过CAS将状态由SIGNAL设为0,如果失败则重新开始循环
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;
                //成功后唤醒等待的线程
                unparkSuccessor(h);
            }
            //如果为默认状态那么尝试通过CAS将状态由0设为PROPAGATE,设置失败则重新开始循环
            else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;
        }
        //如果头结点没有被其它线程更改则跳出循环
        if (h == head)
            break;
    }
}

猜你喜欢

转载自blog.csdn.net/abc123lzf/article/details/82532036
今日推荐