AQS之独占模式和共享模式

AQS之独占模式和共享模式

由于ReentrantLock是一个独占锁,独占锁的知识可以参考AQS之理论知识(一)和AQS之公平锁和非公平锁(二) 两篇文章,本文重点讲解共享模式。并且本文共享模式的讲解以CountDownLatch为主。

一、概念

​ AQS提供了两种工作模式:独占(exclusive)模式和共享(shared)模式。它的所有子类中,要么实现并使用了它独占功能的 API,要么使用了共享功能的API,而不会同时使用两套 API,即便是它最有名的子类 ReentrantReadWriteLock,也是通过两个内部类:读锁和写锁,分别实现的两套 API 来实现的。

​ 独占模式即当锁被某个线程成功获取时,其他线程无法获取到该锁,共享模式即当锁被某个线程成功获取时,其他线程仍然可能获取到该锁。

1.1 独占模式

​ 同一时间只有一个线程能拿到锁执行,锁的状态只有0和1两种情况。

1.2 共享模式

​ 同一时间有多个线程可以拿到锁协同工作,锁的状态大于或等于0。

1.3 API对比

独占模式共享模式
tryAcquire(int arg)tryAcquireShared(int arg)
acquire(int arg)acquireShared(int arg)
acquireQueued(final Node node, int arg)doAcquireShared(int arg)
tryRelease(int arg)tryReleaseShared(int arg)
release(int arg)releaseShared(int arg)

二、AQS重要方法

2.1 acquireShared

​ 该方法的调用链:tryAcquireShared->doAcquireShared。其中tryAcquireShared由子类根据不同的业务需求实现,在Semaphore重要方法具体分析。doAcquireShared在AQS中已经实现,直接调用即可。

/**
 * Acquires in shared uninterruptible mode.
 * @param arg the acquire argument
 */
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);
                /*r>=0代表获取锁成功,线程被唤醒*/
                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);
    }
}

​ doAcquireShared方法的主要路径是addWaiter->tryAcquireShared->setHead()->doReleaseShared(),对应下面的业务大概:

2.1.1 线程信息包装成节点并添加到队尾;
2.1.2 竞争锁资源
2.1.3 获取节点头结点并 放在队列第一个
2.1.4 唤醒当前节点后的节点,如果队列头部被修改,循环唤醒下一个。

2.2 releaseShared

该方法的调用链:tryReleaseShared->doReleaseShared。其中tryReleaseShared由子类根据不同的业务需求实现,在Semaphore重要方法具体分析。doReleaseShared在AQS中已经实现,直接调用即可。

private void doReleaseShared() {
        /*
         * Ensure that a release propagates, even if there are other
         * in-progress acquires/releases.  This proceeds in the usual
         * way of trying to unparkSuccessor of head if it needs
         * signal. But if it does not, status is set to PROPAGATE to
         * ensure that upon release, propagation continues.
         * Additionally, we must loop in case a new node is added
         * while we are doing this. Also, unlike other uses of
         * unparkSuccessor, we need to know if CAS to reset status
         * fails, if so rechecking.
         */
        // 无限循环
        for (;;) {
            // 保存头结点
            Node h = head;
            if (h != null && h != tail) { // 头结点不为空并且头结点不为尾结点
                // 获取头结点的等待状态
                int ws = h.waitStatus; 
                if (ws == Node.SIGNAL) { // 状态为SIGNAL
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) // 不成功就继续
                        continue;            // loop to recheck cases
                    // 释放后继结点
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) // 状态为0并且不成功,继续
                    continue;                // loop on failed CAS
            }
            if (h == head) // 若头结点改变,继续循环  
                break;
        }
    }

​ releaseShared实现了加速唤醒,主要靠h == head这段逻辑实现,例举一种doReleaseSharedh == head不成立的场景,用户层面的线程a释放锁之后,位于队首的线程t1被唤醒,t1调用setHeadAndPropagate方法设置头节点为t1,但还未调用doReleaseShared中的unparkSuccessor方法,这时用户层面的线程b释放锁,唤醒位于队首的线程t2,t2调用setHeadAndPropagate设置新的头节点为t2,这个时候t1继续执行,最后发现队首元素已经变化,继续for循环调用unparkSuccessor方法唤醒队首元素。

doReleaseSharedh == head不成立时进入for循环持续唤醒同步队列中线程的逻辑,主要是一种加速唤醒的优化逻辑,当头节点发生变化时,说明此时有不止一个线程释放锁,而在共享模式下,锁是能够被不止一个线程所持有的,因此应该趋向于唤醒更多同步队列中的线程来获取锁。

三、CountDownLatch重要方法

3.1 概念

​ CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。

​ CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。当计数器的值为0时,表示所有的线程都已经完成一些任务,然后在CountDownLatch上等待的线程就可以恢复执行接下来的任务。

3.2 属性

3.2.1 Sync类

​ 帮助类,同时也是一个抽象类,继承了AQS,同时又根据需要添加了一些方法,供CountDownLatch调用。

3.2.1.1 Sync() 构造方法
//初始化数值用于计数
Sync(int count) {
    setState(count);
}
3.2.1.2 tryAcquireShared
//如果state值等于,说明锁有效,否则锁无效
protected int tryAcquireShared(int acquires) {
  return (getState() == 0) ? 1 : -1;
}
3.2.1.3 tryReleaseShared
 //调用一次,减一次初始值
 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;
            }
}
3.2.1.4 getCount

​ 获得锁的数值。

3.2.2 CountDownLatch()构造方法

该构造函数可以构造一个用给定计数初始化的CountDownLatch,并且构造函数内完成了sync的初始化,并设置了状态数。

    public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }
3.2.3 countDown()

作用:此函数将递减锁存器的计数,如果计数到达零,则释放所有等待的线程。

    public void countDown() {
        sync.releaseShared(1);
    }
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

由代码可见, countDown()主要的流程是tryReleaseShared->doReleaseShared.其中tryReleaseShared方法上文已经简单分析,tryReleaseShared方法每次调用都会讲state值减一,只有减到值为0时,开始执行doReleaseShared(),释放该线程;

3.2.4 await()

此函数将会使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断。

    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }
    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) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

由代码可见,主要的方法调用链是tryAcquireShared->doAcquireSharedInterruptibly。doAcquireSharedInterruptibly和doAcquireShared方法体类似,本文不在讲述,重点讲述下逻辑。

tryAcquireShared小于0时,说明state还未被消费完,线程在代码边界处停留继续消费。

注意:CountDownLatch没有使用队列,仅用了自旋。

四、总结

4.1 区别

4.1.1 节点对象

​ 在Node类中通过nextWaiter来标识共享模式(SHARED)与独占模式(EXCLUSIVE)下的节点。

​ 共享模式的节点对象是

static final Node SHARED = new Node();
Node node = new Node(Thread.currentThread(), mode);       

​ 独占模式的节点对象是

 static final Node EXCLUSIVE = null;
 Node node = new Node(Thread.currentThread(), mode);
4.1.2 线程唤醒的时机

​ 共享模式下,头节点获取共享锁后可以立即唤醒后继节点,而不用等待获取共享锁后释放再唤醒,唤醒后继线程有2处,一处是获取到共享锁后可以立即唤醒后续的线程,但是后续线程必须是共享模式的线程;第二处是在释放锁后唤醒后继线程,这边我认为释放锁后唤醒的后继线程可以包含独占模式,但是前提是所有的独占模式前面所有的共享模式锁都已经释放。

  • 6
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值