JUC并发基石之AQS源码解析--共享锁的获取与释放

一:简介

前面我们以分析了AQS独占锁的获取与释放,本篇我们来看看共享锁,AQS对于共享锁与独占锁的实现框架还是比较类似的。

共享锁与独占锁的区别在于,独占锁是独占的,因此当独占锁已经被某个线程持有时,其他线程只能等待它被释放后,才能去争锁,并且同一时刻只有一个线程能争锁成功。

而对于共享锁而言,它可以被多个线程同时持有,因此如果一个线程成功获取了共享锁,那么其他等待在这个共享锁上的线程就也可以尝试去获取锁,而不需要等到该节点释放锁的时候。所以,在共享锁模式下,在获取锁和释放锁结束时,都会唤醒后继节点,这与独占锁有很大的区别。

二:源码分析

直接来看共享锁的获取吧:

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

对于共享锁的tryAcquireShared(int acquires)方法,它的返回值是个整型值:

  1. 如果该值小于0,则代表当前线程获取共享锁失败。
  2. 如果该值大于0,则代表当前线程获取共享锁成功,并且接下来其他线程尝试获取共享锁的行为很可能成功。
  3. 如果该值等于0,则代表当前线程获取共享锁成功,但是接下来其他线程尝试获取共享锁的行为会失败。

因此,只要该返回值大于等于0,就表示获取共享锁成功。

我们来看Semphore里面非公平的实现:

        protected int tryAcquireShared(int acquires) {
            return nonfairTryAcquireShared(acquires);
        }
        
        final int nonfairTryAcquireShared(int acquires) {
            for (;;) {
                // 获取剩余的可用数
                int available = getState();
                // 计算剩余的与需求的差值
                int remaining = available - acquires;
                // 差值小于0,说明不够用,直接返回,也就是获取锁失败
                // 否则CAS尝试获取
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    // 返回剩余值,大于等于0,获取成功,小于0,获取失败
                    return remaining;
            }
        }

接下来我们看看doAcquireShared方法:

    private void doAcquireShared(int arg) {
        // 把线程封装成共享节点
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                // 拿到当前节点的前驱节点
                final Node p = node.predecessor();
                // 如果前驱节点是head节点,说明当前节点是队列中第一个等待锁的节点
                // 那么尝试再次获取锁
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        // 获取锁成功,把当前节点设为head节点,并且唤醒后继节点
                        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);
        }
    }

我们看到与独占锁的获取框架大致相同,首先,获取锁失败后,把线程封装成节点,不过这里是封装成共享节点,之后如果前驱节点是head节点,说明当前节点是队列中第一个等待锁的节点,那么尝试再次获取锁,如果获取锁成功,把当前节点设为head节点,但是会唤醒后继节点,这里与独占锁不同,最后获取锁失败,或者队列前面有其它节点在等待锁,判断是否需要把当前线程挂起。

第一点不同就是独占锁的acquireQueued调用的是addWaiter(Node.EXCLUSIVE),而共享锁调用的是addWaiter(Node.SHARED),表明了该节点处于共享模式,这两种模式的定义为:

/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;

Node被赋值给了节点的nextWaiter属性,它只起到标记作用,用作判断节点是否处于共享模式:

Node(Thread thread, Node mode) {     // Used by addWaiter
    this.nextWaiter = mode;
    this.thread = thread;
}

final boolean isShared() {
    return nextWaiter == SHARED;
}

这里的第二点不同就在于获取锁成功后的行为,对于独占锁而言,如果获取锁成功,会把当前节点设为head节点,但是对于共享锁,还会唤醒后继节点。

我们来看setHeadAndPropagate(node, r)方法:

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

在共享锁模式下,锁可以被多个线程所共同持有,既然当前线程已经拿到共享锁了,那么就可以直接通知后继节点来拿锁,而不必等待锁被释放的时候再通知。

共享锁的释放:

我们来看releaseShared(int arg)方法:

public final boolean releaseShared(int arg) {
        // CAS释放锁的状态成功
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

如果CAS释放锁的状态成功,那么调用doReleaseShared()方法:

上面我们分析过获取共享锁成功时,也可能会调用到doReleaseShared。也就是说,获取共享锁的线程和释放共享锁的线程可能在同时执行这个doReleaseShared。

    private void doReleaseShared() {
        for (;;) {
            // 用h来保存头节点
            Node h = head;
            // head != tail 说明队列中有节点在等待
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                // 后继节点需要被唤醒
                if (ws == Node.SIGNAL) {
                    // 采用CAS操作先将Node.SIGNAL状态改为0
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                       // CAS失败就重试 
                       continue;            // loop to recheck cases
                    // CAS成功唤醒后继节点
                    unparkSuccessor(h);
                }
                // ws为0是指当前队列的最后一个节点成为了头节点
                else if (ws == 0 &&
                         // CAS失败,ws此时已经不为0了
                         // 说明有新的节点入队了,ws的值被改为了Node.SIGNAL
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            // head还是原来的那个head
            if (h == head)                   // loop if head changed
                break;
        }
    }

doReleaseShared使用了循环,每次循环中重新读取一次head,然后保存在h中,再配合if(h == head) break; 这样,如果检测到head没有变化时就会退出循环,不然会执行多次循环。head改变,说明锁可以被获取,那么多次循环主要是为了提升效率,快速唤醒线程。

那什么时候head会变化呢? 当有一个thread成功获取锁,然后setHead设置了新head。

当队列至少有两个node,如果状态为SIGNAL,说明h的后继是需要被通知的。通过CAS对节点的状态进行更改。只要head成功得从SIGNAL修改为0,那么head的后继的线程会被唤醒。

如果状态为0,说明h的后继所代表的线程已经被唤醒或即将被唤醒,并且这个中间状态即将消失,但是CAS设置head的status为PROPAGATE失败,要么由于acquire thread获取锁失败再次设置head为SIGNAL并再次阻塞,要么由于acquire thread获取锁成功而将自己(head后继)设置为新head并且只要head后继不是队尾,那么新head肯定为SIGNAL。所以设置这种中间状态的head的status为PROPAGATE,让其status又变成负数,这样可能被被唤醒线程检测到。

如果状态为PROPAGATE,直接判断head是否变化。

两个continue保证了进入那两个分支后,只有当CAS操作成功后,才可能去执行if(h == head) break;,才可能退出循环。

三:总结
共享锁的调用框架和独占锁很相似,它们最大的不同在于获取锁的逻辑——共享锁可以被多个线程同时持有,而独占锁同一时刻只能被一个线程持有。
由于共享锁同一时刻可以被多个线程持有,因此当头节点获取到共享锁时,可以立即唤醒后继节点来争锁,而不必等到释放锁的时候。因此,共享锁触发唤醒后继节点的行为可能有两处,一处在当前节点成功获得共享锁后,一处在当前节点释放共享锁后。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值