zoukankan      html  css  js  c++  java
  • 并发编程学习笔记(十三、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、释放资源:同样的共享锁会唤醒队友,而独占锁不会。

  • 相关阅读:
    python目录操作shutil
    python os.walk
    利用华为eNSP模拟器实现vlan之间的通信
    Python之道1-环境搭建与pycharm的配置django安装及MySQL数据库配置
    利用Excel做一些简单的数据分析
    Django中的枚举类型
    django使用model创建数据库表使用的字段
    ps 命令的十个简单用法
    goinception安装
    docker安装redis 指定配置文件且设置了密码
  • 原文地址:https://www.cnblogs.com/bzfsdr/p/13141361.html
Copyright © 2011-2022 走看看