并发编程学习笔记(十三、AQS同步器源码解析2,AQS共享锁)

目录:

  • 共享锁和独占锁的区别
  • 共享锁实现原理
  • 共享锁和独占锁在源码上有何区别

共享锁和独占锁的区别

共享锁和独占锁(排它锁)最大的区别就是,在同一时刻能否有多个线程获取同步状态

  • 独占模式,获取资源后,只有一个线程获取同步状态并执行。
  • 共享模式,在获取资源后,多个线程共同执行。

共享锁实现原理

1、加锁:

共享锁和排它锁的实现原理类似,我这次就不具体说明了,直接上源码。

1 /**
2  * 共享模式,获取资源
3  */
4 public final void acquireShared(int arg) {
5     // 成功获取资源则结束,失败则进入等待队列
6     // 小于0的都任务是获取资源失败,反之大于等于0则是成功
7     if (tryAcquireShared(arg) < 0)
8         doAcquireShared(arg);
9 }

同样的tryAcquireShared()也是交给子类实现的加锁函数,可参照CountDownLatch

1 protected int tryAcquireShared(int arg) {
2     throw new UnsupportedOperationException();
3 }
1 /**
2  * CountDownLatch:当状态为0的时候则可以获取锁(共享资源的状态),反之则不能
3  */
4 protected int tryAcquireShared(int acquires) {
5     return (getState() == 0) ? 1 : -1;
6 }

——————————————————————————————————————————————————————————————————————

接下来我们来看下获取资源失败,进入队列等待执行了什么逻辑。

 1 private void doAcquireShared(int arg) {
 2     // 自旋添加共享模式待队尾,与独占模式一致,只是入参不同
 3     final Node node = addWaiter(Node.SHARED);
 4     boolean failed = true;
 5     try {
 6         boolean interrupted = false;
 7         for (;;) {
 8             final Node p = node.predecessor();
 9             if (p == head) {
10                 int r = tryAcquireShared(arg);
11                 // r >= 0标识共享资源获取成功
12                 if (r >= 0) {
13                     // 将当前结点设置为头结点,并检查后继节点是否在共享模式下等待
14                     setHeadAndPropagate(node, r);
15                     p.next = null; // help GC
16                     if (interrupted)
17                         selfInterrupt();
18                     failed = false;
19                     return;
20                 }
21             }
22             // 与独占模式类似,校验是否需要阻塞线程,判断中断状态
23             if (shouldParkAfterFailedAcquire(p, node) &&
24                 parkAndCheckInterrupt())
25                 interrupted = true;
26         }
27     } finally {
28         if (failed)
29             cancelAcquire(node);
30     }
31 }

哈哈,是不是和独占锁很像,的确!

但也有些不同,共享锁是只有线程是head.next时,也就是线程为头节点的后继节点时才会去尝试获取资源,如果还有其它节点还会唤醒之后的线程

就这样说你可能会有些疑惑,我来解释下;首先会自旋,也就是会一直轮询第8行那块,直到满足第9行才会去执行第10行;也就是说第8行的p一定是head,而p的值是当前节点的前驱结点,所以p肯定为head的后继节点。

1 final Node predecessor() throws NullPointerException {
2     Node p = prev;
3     if (p == null)
4         throw new NullPointerException();
5     else
6         return p;
7 }

——————————————————————————————————————————————————————————————————————

上面说到如果还有其它节点还会唤醒之后的线程,那么问题来了。

假如老大用完后释放了5个资源,而老二需要6个,老三需要1个,老四需要2个。 老大先唤醒老二,老二发现自己的资源都不够,老二是否把资源让给老三呢?

答案是否定的,老二会继续park()等待其它线程释放资源,更不会唤醒老三和老四。

——————————————————————————————————————————————————————————————————————

接下来就是我们共享锁的重点部分了,setHeadAndPropagate()。

 1 /**
 2  * 成为头节点,并在满足条件时唤醒后继节点
 3  */
 4 private void setHeadAndPropagate(Node node, int propagate) {
 5     Node h = head; // Record old head for check below
 6     // 把当前节点设为头节点
 7     setHead(node);
 8     /*
 9      * 满足以下三种情况执行唤醒操作:
10      * 1、propagate > 0,标识后继节点需要被唤醒(AQS默认只有一个是大于0的,就是线程取消,CANCELLED)
11      * 2、原头节点为null或ws < 0
12      * 3、新的头节点(也就是当前节点)为null或ws < 0
13      */
14     if (propagate > 0 || h == null || h.waitStatus < 0 ||
15         (h = head) == null || h.waitStatus < 0) {
16         Node s = node.next;
17         // 若s == null或s是共享模式,则唤醒后继线程
18         if (s == null || s.isShared())
19             doReleaseShared();
20     }
21 }

这部分也不是很难,你应该能很快的理解它。

——————————————————————————————————————————————————————————————————————

在上述setHeadAndPropagate()中说道满足特定条件且是共享模式时会唤醒后继线程,也就是调用doReleaseShared(),这其实就是一个解锁的过程,我们来看看具体的实现。

 1 private void doReleaseShared() {
 2     /*
 3      * 自旋后释放后继节点
 4      */
 5     for (;;) {
 6         Node h = head;
 7         // 表示队列中至少有2个节点
 8         if (h != null && h != tail) {
 9             int ws = h.waitStatus;
10             // ws == SIGNAL(后继节点需要被唤醒)
11             if (ws == Node.SIGNAL) {
12                 // 通过CAS设置h的waitStatus字段为0
13                 if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
14                     // CAS失败则继续自旋
15                     continue;            // loop to recheck cases
16                 // CAS成功,则唤醒后继节点,执行过程同独占模式下的唤醒流程
17                 unparkSuccessor(h);
18             }
19             // 同上,若h是初始状态0,则CAS为PROPAGATE(表示锁的下一次获取可以“无条件传播”)
20             else if (ws == 0 &&
21                      !h.compareAndSetWaitStatus(0, Node.PROPAGATE))
22                 continue;                // loop on failed CAS
23         }
24         // 自旋的跳出条件,head不变则跳出,变化则一直自旋
25         // head不变表示设置完成,可以退出循环
26         // head变化,可能被唤醒的其它节点重新设置了头节点,这样头节点发生了变化,需要进行重试,保证可以传播唤醒型号
27         if (h == head)                   // loop if head changed
28             break;
29     }
30 }

至此acquireShared()已经解析完成了,我们来总结下:

1、首先tryAcquireShared()尝试获取资源,成功则直接返回;失败则通过doAcquireShared()进入等待队列park()直到被unpark()/intemupt()并成功获取到资源才返回。整个等待过程也是忽略中断的

2、其实跟acquire()的流程大同小异,只不过多了个自己拿到资源后还会去唤醒后继队友的操作(这才是共享嘛)。 

——————————————————————————————————————————————————————————————————————

2、解锁:

解锁这个东西啊你只要把上面弄懂了,就是小事情了,我不再赘述了,哈哈。

1 public final boolean releaseShared(int arg) {
2     if (tryReleaseShared(arg)) {
3         doReleaseShared();
4         return true;
5     }
6     return false;
7 }

共享锁和独占锁在源码上有何区别

1、获取资源

  • 共享锁(doAcquireShared()):
    • 只有线程是head.next时,也就是线程为头节点的后继节点时才会去尝试获取资源。
    • 如果还存在其它节点,还会唤醒之后的线程,也就是自己执行完之后还会叫队友来获取资源。
  • 独占锁(acquireQueued()):
    • 同共享锁,但不仅要线程为头节点的后继节点,还需要获取到锁
    • 独占锁执行完后就结束了,不会唤醒队友(脏线男枪,哈哈哈哈哈哈)。

2、释放资源:同样的共享锁会唤醒队友,而独占锁不会。

posted @ 2020-06-16 22:56  被猪附身的人  阅读(267)  评论(0编辑  收藏  举报