JUC并发编程之AQS源码解析(共享锁)

本文详细解析了Java并发工具类CountDownLatch的内部实现,特别是共享锁的获取与释放过程。通过分析`acquireShared()`、`tryAcquireShared()`、`doAcquireSharedInterruptibly()`等方法,阐述了共享锁如何允许多个线程同时访问,以及在资源释放后如何唤醒等待队列中的后续线程。总结了共享锁与独占锁的主要区别在于唤醒机制的不同。
摘要由CSDN通过智能技术生成

上一篇谈到独占锁,共享锁和独占锁有很多相似之处,接下来进行分析.......

1.什么是共享锁和独占锁?

共享锁就是允许多个线程同时获取一个锁,一个锁可以同时被多个线程拥有。

排它锁,也称作独占锁,一个锁在某一时刻只能被一个线程占有,其它线程必须等待锁被释放之后才可能获取到锁。

2.以CountDownLatch为进行源码解析

①首先new CountDownLatch(count),会创建一个Sync类,并设置同步状态值为count

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

重点:

占用锁

②但我们调用await()方法时线程会尝试去占用锁

public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    // 1.如果线程被中断抛出异常
    if (Thread.interrupted())
        throw new InterruptedException();
    // 2.尝试占用锁,此处也使用了模板模式,需要AQS的实现者去自己编写占用锁的业务逻辑
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}

③tryAcquireShared(int acquires) 尝试占用共享锁

规定:一、返回值小于0表示获取锁失败,需要进入等待队列。二、如果返回值等于0表示当前线程获取共享锁成功,但它后续的线程是无法继续获取的,也就是不需要把它后面等待的节点唤醒。最后、如果返回值大于0,表示当前线程获取共享锁成功且它后续等待的节点也有可能继续获取共享锁成功,也就是说此时需要把后续节点唤醒让它们去尝试获取共享锁。

protected int tryAcquireShared(int acquires) {
    // 如果state还没被减为0,就返回-1,获取锁失败,如果=0,表明同步资源数量已经被减为0了,返回1,获取锁成功,唤醒继任线程
    return (getState() == 0) ? 1 : -1;
}

④doAcquireSharedInterruptibly(int arg)获取锁失败,入队,阻塞等待被唤醒

private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    // 1.创建共享类型的节点,并进入等待队列(与独占锁逻辑相同)
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
      // 2.如果当前节点是继任节点,再次尝试占用锁
            if (p == head) {
                int r = tryAcquireShared(arg);
      // 3.r >= 0表示占用成功,需要去设置头节点并且传播唤醒继任节点的下一个节点
                if (r >= 0) {
       //4.这个方法很重要,在下面分析
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
        // 5.设置当前节点的前节点的waitState为SIGNAL,
        //    并进行阻塞
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

setHeadAndPropagate(Node node, int propagate)设置当前被唤醒的节点为头节点并唤醒它的下一个节点

private void setHeadAndPropagate(Node node, int propagate) {
    // 1.获取头节点(哨兵节点)
    Node h = head;
    // 2.将继任节点设为头节点(与独占锁相同)
    //注:这里是获取到锁之后的操作,不需要并发控制
    setHead(node);
    // 3.符合以下情况需要执行唤醒操作:
    // propagate > 0 表示占用锁成功,并需要去传播唤醒下一个节点
    // h.waitStatus < 0 表示当前占用锁的节点的下一个节点已经准备好被唤醒了
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) 
        Node s = node.next;
    // 4.如果当前节点后面没有节点了 或者下一个节点是共享类型的节点(我们创建的时候就是以共享模式创建的节点),就进行唤醒操作
        if (s == null || s.isShared())
    // 5.执行唤醒操作(很重要)
            doReleaseShared();
    }
}

doReleaseShared()执行唤醒继任节点(头节点,前面已经设置过了,将当前节点设为头节点)的下一个节点

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
         // 1.获取头节点(也就是当前节点,也就是被唤醒的继任节点)的waitStatus==-1(ws == Node.SIGNAL,前面我们分析过,一个等候队列中的节点,在正式阻塞等待之前,一定会将它的前一个节点的waitStatus设为SIGNAL状态),说明它的下一个节点,已经阻塞等待被唤醒了
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
         // 2.将头节点的waitStatus设回0,表明该节点已经不处于阻塞等候资源的状态了,而是获取到了资源
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
           // 3.修改头节点waitStatus成功,即可唤醒下一个节点了
                unparkSuccessor(h);
            }
          // 4.如果当前头结点的下一个节点还没准备好(还没有进入阻塞),用CAS把当前节点的等候状态设为PROPAGATE,确保当下一个节点准备好时可以传下去继续唤醒
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        // 5.如果头节点没有改变,表示下一个节点唤醒成功,退出循环 
        // 如果头节点发生了变化,比如说其他线程获取到了锁,为了使自己的唤醒动作可以传递,必须进行重试(因为只有当前节点是头节点,它的下一个被唤醒的节点,才能通过以下判断传递唤醒
        /**
            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 (h == head) 
            break;
    }
}

unparkSuccessor(h) 唤醒下一个节点与独占锁的逻辑一样,就是唤醒头节点的下一个节点(不做分析)

释放锁

⑤当调用countDownLatch的countDown()方法时,如果同步状态为0,表明没有线程占用了,会去释放锁

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        // 就是上面唤醒继任节点的方法
        doReleaseShared();
        return true;
    }
    return false;
}

tryReleaseShared(int releases)将同步状态减少1,检查同步资源的占用情况

    protected boolean tryReleaseShared(int releases) {
        for (;;) {
            int c = getState();
            if (c == 0)
                return false;
            int nextc = c-1;
            // 如果同步状态-1以后,变为了0,说明可以唤醒阻塞的节点了,返回ture,若减1之后同步状态数量仍大于0,也就是说明仍然被占用,返回false,表明不可以执行唤醒操作
            if (compareAndSetState(c, nextc))
                return nextc == 0;
        }
    }
}

3.执行流程总结:

获取锁的过程:

  1. 当线程调用acquireShared()申请获取锁资源时,如果成功,则进入临界区。

  2. 当获取锁失败时,则创建一个共享类型的节点并进入一个FIFO等待队列,然后被挂起等待唤醒。

  3. 当队列中的等待线程被唤醒以后就重新尝试获取锁资源,如果成功则唤醒后面还在等待的共享节点并把该唤醒事件传递下去,即会依次唤醒在该节点后面的所有共享节点,然后进入临界区,否则继续挂起等待。

释放锁过程:

  1. 当线程调用releaseShared()进行锁资源释放时,如果释放成功,则唤醒队列中等待的节点,如果有的话。

4.总结

共享锁和独占锁相比主要区别在于:当处在等待队列中的一个共享节点获得了锁之后,可以依次唤醒它后面的所有已经进入阻塞状态的共享节点

而独占锁呢:当处在等待队列中的一个节点获得锁了之后,后面处于阻塞状态的继任节点,必须等待它释放了锁,才可能被唤醒

如有问题欢迎指正

原文:https://www.cnblogs.com/lbys/p/14279017.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值