AQS源码分析之独占锁和共享锁

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/luofenghan/article/details/75065001

简介

AQS实现锁机制并不是通过synchronized——给对象加锁实现的,事实上它仅仅是一个工具类!它没有使用更高级的机器指令,也不靠关键字,更不依靠JDK编译时的特殊处理,仅仅作为一个普普通通的类就完成了代码块的访问控制。

AQS使用标记位+队列的方式,记录获取锁、竞争锁、释放锁等一些类锁操作。但更准确的说,AQS并不关心什么是锁,对于AQS来说它只是实现了一系列的用于判断资源是否可以访问的API,并且封装了在访问资源受限时,将请求访问的线程加入队列、挂起、唤醒等操作。AQS关心的问题如下:

  1. 资源不可访问时,怎么处理?
  2. 资源时可以被同时访问,还是在同一时间只能被一个线程访问?
  3. 如果有线程等不及资源了,怎么从AQS队列中退出?

至于资源能否被访问的问题,则交给子类去实现。

站在使用者的角度,AQS的功能主要分为两类:独占锁共享锁。在它的所有子类中,要么实现了它的独占功能的API,要么实现了共享功能的API,但不会同时使用两套API,即使是ReentrantReadWriteLock,也是通过两个内部类:读锁和写锁,分别使用两套API来实现的。

  • 当AQS的子类实现独占功能时,如ReentrantLock,资源是否可以被访问被定义为:只要AQS的state变量不为0,并且持有锁的线程不是当前线程,那么代表资源不可访问。
  • 当AQS的子类实现共享功能时,如CountDownLatch,资源是否可以被访问被定义为:只要AQS的state变量不为0,那么代表资源不可以为访问。

AQS类继承结构图

image

独占锁

ReentrantLock是AQS独占功能的一个实现,通常的使用方式如下:

reentrantLock.lock();
// do something
reentrantLock.unlock();

ReentrantLock会保证执行do something在同一时间有且只有一个线程获取到锁,其余线程全部挂起,直到该拥有锁的线程释放锁,被挂起的线程被唤醒重新开始竞争锁。

ReentrantLock的加锁全部委托给内部代理类完成,ReentrantLock只是封装了统一的一套API而已,而ReentrantLock又分为公平锁非公平锁

abstract static class Sync extends AbstractQueuedSynchronizer {}
static final class NonfairSync extends Sync {}
static final class FairSync extends Sync {}
  • 公平锁:每个线程抢占锁的顺序为先后调用lock方法的顺序,并依此顺序获得锁,类似于排队吃饭;
  • 非公平锁:每个线程抢占锁的顺序不变,谁运气好,谁就获得锁,和调用lock方法的先后顺序无关,类似后插入。

换句话说,公平锁和非公平锁的唯一的区别是在获取锁的时候是直接去获取锁,还是进入队列排队的问题。

获取独占锁的流程

image

FairSync的tryAcquire()方法分析

   protected final boolean tryAcquire(int acquires) {
       final Thread current = Thread.currentThread();/*获取当前线程*/
       int c = getState(); /*获取父类AQS中的标志位*/
       if (c == 0) {
           if (!hasQueuedPredecessors()
                   /*说明队列中没有其他线程:没有线程正在占有锁,那么修改一下状态位*/
                   //注意:这里的acquires是在lock的时候传递来的,从上面的图中可以知道,这个值是写死的1
                   && compareAndSetState(0, acquires)) {
               // 如果通过CAS操作将状态为更新成功则代表当前线程获取锁,
               // 因此,将当前线程设置到AQS的一个变量中,说明这个线程拿走了锁。
               setExclusiveOwnerThread(current);
               return true;/*返回true,说明已拿到锁*/
           }
       } else if (current == getExclusiveOwnerThread()) {
           /*如果当前state不为0,说明锁已经被拿走,那么判断是否是当前线程拿走了锁*/
           /*因为锁是可重入的,可以重复lock,unlock*/
           int nextc = c + acquires;
           if (nextc < 0)
               throw new Error("Maximum lock count exceeded");
           setState(nextc);
           return true;
       }
       return false;
   }

如果当前线程获取到锁,tryAcquire()返回true,否则返回false,这时会返回到AQS的acquire()方法。

如果没有获取到锁,那么应该将当前线程放到队列中去,只不过,在放之前,需要做些包装。

AQS的addWaiter()方法分析

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {
        /*将节点加入到队列尾部*/
        node.prev = pred;
        /*尝试使用CAS方式修改尾节点,但是在【并发情况】下,可能修改失败*/
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    /*修改失败,说明有并发,那么进入enq,以自旋方式修改*/
    enq(node);
    return node;
}

用当前线程去构造一个Node对象,mode是一个表示Node类型的字段(独占的还是共享的)。构造好节点后,在队列不为空的时候,使用CAS的方式将新的节点加入队列尾部,如果修改失败,则进入enq()方法,使用自旋的方式修改。

/**
 * 将节点通过自旋的方式插入到队列尾部
 *
 * @param node the node to insert
 * @return node's predecessor
 */
private Node enq(final Node node) {
    for (; ; ) {
        Node pred = tail;
        if (pred == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return pred;
            }
        }
    }
}

将线程的节点接入到队里中后,当然还需要做一件事:将当前线程挂起!这个事,由acquireQueued()来做。

AQS的acquireQueued()方法分析

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (; ; ) {
            /*获得当前节点pred节点*/
            final Node p = node.predecessor();
            /*如果当前节点正好是第二个节点,那么则再次尝试获取锁*/
            if (p == head && tryAcquire(arg)) {
                /*获取锁成功,*/
                setHead(node); /*将当前节点设置为头结点*/
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            /*当前节点不是第二个节点 或者 再次获取锁失败*/
            /*判断是否需要挂起,在挂起后,判断线程是否中断*/
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

判断此时是否能够安全的挂起

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * 当前节点已经被设置为等待唤醒的状态,可以安全的挂起了
         */
        return true;
    if (ws > 0) {
        /*
         * 当前节点node的前任节点被取消,那么【跳过】这些取消的节点,
         * 当跳过之后,重新尝试获取锁
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * 通过前面的判断,waitStatus一定不是 SIGNAL 或 CANCELLED。
         * 推断出一定是 0 or PROPAGATE
         * 调用者需要再次尝试,在挂起之前能不能获取到锁,
         * 因此,将当前pred的状态设为SIGNAL,再次尝试获取锁之后,如果还没有得到锁那么挂起
         *
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

挂起线程,并判断此时线程是否被中断。

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    /*如果被中断,则返回true,interrupted()方法返回后,中断状态被取消,变为false*/
    return Thread.interrupted();
}

到此为止,一个线程对于锁的一次竞争才告一段落,结果有两种:

  • 要么成功获取到锁(不用进入到AQS队列中);
  • 要么获取失败,被挂起,等待下次唤醒后继续循环尝试获取锁。

值得注意的是,AQS的队列为FIFO队列,所以,每次被CPU假唤醒,且当前线程不是处在头节点的位置,也是会被挂起的。AQS通过这样的方式,实现了竞争的排队策略。

释放独占锁流程分析

image

AQS的release()方法分析

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            /*将持有锁的头结点释放成功后
            * 唤醒其后继节点
            * */
            unparkSuccessor(h);
        return true;
    }
    return false;
}

尝试释放锁,如果释放成功,找到AQS的头节点,调用unparkSuccessor()唤醒FIFO队列中第一个等待锁的节点。

Sync的tryRelease()方法分析

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    //如果【释放的线程】和【获取锁的线程】不是同一个,抛出非法监视器状态异常。
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();

    boolean free = false;
    // 因为是重入的关系,不是每次释放锁c都等于0,
    // 直到最后一次释放锁时,才通知AQS不需要再记录哪个线程正在获取锁。
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

AQS的unparkSuccessor()方法分析

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0) {
        /*如果状态为负(SIGNAL、PROPAGATE),那么清除其状态
        * 如果失败,或者状态被其他等待线程改变,也没有关系
        * */
        compareAndSetWaitStatus(node, ws, 0);
    }
    /*
     * 一般情况下唤醒的线程是【头结点】的【下一个节点】
     * 但是如果该节点被取消或者为null,
     * 那么需要【从后往前遍历】寻找一个【最早的】并且【没有被取消】的节点
     */
    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;
    }
    if (s != null) LockSupport.unpark(s.thread);
}

当FIFO队列中等待锁的第一个节点被唤醒之后,会返回到到节点所在线程的acquireQueued()方法中,继续下一轮循环,这时当前节点正好时头节点的第一个后继节点,并且使用CAS修改状态持有锁成功,那么当前节点则晋升为头结点,并返回。

····

for (; ; ) {
    /*获得当前节点pred节点*/
    final Node p = node.predecessor();
    /*如果当前节点正好是第二个节点,那么则再次尝试获取锁*/
    if (p == head && tryAcquire(arg)) {
        /*获取锁成功,*/
        setHead(node); /*将当前节点设置为头结点*/
        p.next = null; // help GC
        failed = false;
        return interrupted;
    }
    /*当前节点不是第二个节点 或者 再次获取锁失败*/
    /*判断是否需要挂起,在挂起后,判断线程是否中断*/
    if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
        interrupted = true;
}

····

以上的分析是基于ReentrantLock内部的公平锁来分析的,并且其lcok和unlock已经基本分析完毕,唯独剩下一个非公平锁nonfairSync。其实,它和公平锁的唯一区别就是获取锁的方式不同,一个是按前后顺序一次获取锁,一个是抢占式的获取锁。

  • 非公平锁的lock方法的处理方式:在lock的时候先直接CAS修改一次state变量(尝试获取锁),成功就返回,不成功再排队,从而达到不排队直接抢占的目的。
  • 而对于公平锁:则是老老实实的开始就走AQS的流程排队获取锁。如果前面有人调用过其lock方法,则排在队列中前面,也就更有机会更早的获取锁,从而达到“公平”的目的。

共享锁

共享功能的主要实现为CountDownLatch,CountDownLatch是一种灵活的闭锁实现,它可以使一个或多个线程等待一组事件发生。闭锁状态包括一个计数器,该计数器被初始化为一个正数,表示需要等待的事件数量。countDown递减计数器,表示有一个事件已经发生了,而await方法等待计数器达到零,这表示所有需要等待的时间都已经发生。如果计数器值非零,那么await会一直阻塞直到计数器为零,或者等待线程中断,或者等待超时。

等待获取共享锁的流程分析

image

CountDownLatch的await()方法分析

public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

CountDownLatch的await()方法不会只在一个线程中调用,多个线程可以同时等待await()方法返回,所以CountDownLatch被设计成实现tryAcquireShared()方法,获取的是一个共享锁,锁在所有调用await()方法的线程间共享,所以叫做共享锁。

AQS的acquireSharedInterruptibly()方法分析

public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    /*用于响应线程中断,
    * 在前两行会检查线程是否被打断
    * */
    if (Thread.interrupted())
        throw new InterruptedException();
    /*返回了-1,说明state不为0,也就是CountDownLatch的计数器还不为0*/
    if (tryAcquireShared(arg) < 0)/*尝试获取共享锁,如果小于0,表示获取失败*/
        doAcquireSharedInterruptibly(arg);
}

Sync的tryAcquireShared()方法分析

protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}

AQS的doAcquireSharedInterruptibly()方法分析

private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
    /*将当前线程包装为一个共享节点*/
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (; ; ) {
            final Node p = node.predecessor();
            if (p == head) {
                //如果新建节点的前一个节点,就是Head,
                //说明当前节点是AQS队列中等待获取锁的第一个节点,
                //按照FIFO的原则,可以直接尝试获取锁。
                int r = tryAcquireShared(arg);
                if (r >= 0) {

                    /*如果获取成功,则需要将当前节点设置为AQS队列中的第一个节点*/
                    /*队列的头节点表示正在获取锁的节点*/
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            //检查下是否需要将当前节点挂起
            if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

获得共享锁的流程分析

image

AQS的releaseShared()方法分析

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}
protected boolean tryReleaseShared(int releases) {
    // Decrement count; signal when transition to zero
    for (; ; ) {
        int c = getState();
        if (c == 0)
            return false;
        int nextc = c - 1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

死循环更新state的值,实现state的减1操作,之所以用死循环是为了确保state值的更新成功。

从上文的分析中可知,如果state的值为0,在CountDownLatch中意味:所有的子线程已经执行完毕,这个时候可以唤醒调用await()方法的线程了,而这些线程正在AQS的队列中,并被挂起的,所以下一步应该去唤醒AQS队列中的头节点了(AQS的队列为FIFO队列),然后由头节点去依次唤醒AQS队列中的其他共享节点。

AQS的doReleaseShared()方法分析

private void doReleaseShared() {
    for (; ; ) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                //如果当前节点是SIGNAL意味着,它正在等待一个信号。或者说,它在等待被唤醒,因此做两件事
                /*重置waitStatus标志位,如果失败则重试*/
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases

                /*重置成功后,唤醒等待获取共享锁的第一个节点*/
                unparkSuccessor(h);
            } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                //如果本身头节点的waitStatus是处于重置状态(waitStatus==0)的,将其设置为“传播”状态。
                //意味着需要将状态向后一个节点传播。
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

当FIFO队列中等待共享锁的第一个节点被头结点唤醒之后,会返回到到节点对应线程的doAcquireSharedInterruptibly()方法中,并继续循环,这时候当前节点的前驱节点正好时头结点,并且能够获得共享锁,这时会执行setHeadAndPropagate()方法,将当前节点设置为头结点,并继续唤醒下一个节点。

AQS的setHeadAndPropagate()方法分析

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();
    }
}

首先,使用CAS更换了头结点,然后,将当前节点的下一个节点取出来。如果下一个节点同样是shared类型,再做一个releaseShared()操作。这时可以回到第二步,依次唤醒AQS队列中其他共享节点。

总而言之,AQS关于共享锁方面的实现方式:如果获取共享锁失败后,将请求共享锁的线程封装成Node对象放入AQS的队列中,并挂起Node对象对应的线程,实现请求锁线程的等待操作。待共享锁可以被获取后,从头节点开始,依次唤醒头节点及其以后的所有共享类型的节点。实现共享状态的传播。

共享锁与独占锁的对比

  1. 与AQS的独占功能一样,共享锁是否可以被获取的判断为空方法,交由子类去实现。
  2. 与AQS的独占功能不同,当锁被头节点获取后,独占功能是只有头节点获取锁,其余节点的线程继续沉睡,等待锁被释放后,才会唤醒下一个节点的线程,而共享功能是只要头节点获取锁成功,就在唤醒自身节点对应的线程的同时,继续唤醒AQS队列中的下一个节点的线程,每个节点在唤醒自身的同时还会唤醒下一个节点对应的线程,以实现共享状态的“向后传播”,从而实现共享功能。

参考资料

  1. 深度解析Java 8:JDK1.8 AbstractQueuedSynchronizer的实现分析(上)
  2. 深度解析Java 8:AbstractQueuedSynchronizer的实现分析(下)
展开阅读全文

没有更多推荐了,返回首页