69

Java并发之AQS源码分析(二) 原 荐

 4 years ago
source link: https://my.oschina.net/objcoding/blog/3047164?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Java并发之AQS源码分析(二)

  Java科代表 发布于 5分钟前

字数 1173

阅读 1

收藏 0

Java CAS

面试:你懂什么是分布式系统吗?Redis分布式锁都不会?>>> QNJNvmJ.png!web

我在 Java并发之AQS源码分析(一) 这篇文章中,从源码的角度深度剖析了 AQS 独占锁模式下的获取锁与释放锁的逻辑,如果你把这部分搞明白了,再看共享锁的实现原理,思路就会清晰很多。下面我们继续从源码中窥探共享锁的实现原理。

共享锁

获取锁

public final void acquireShared(int arg) {
  // 尝试获取共享锁,小于0表示获取失败
  if (tryAcquireShared(arg) < 0)
    // 执行获取锁失败的逻辑
    doAcquireShared(arg);
}

这里的 tryAcquireShared 方法是留给实现方去实现获取锁的具体逻辑的,我们主要看 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();
      if (p == head) {
        // 再次尝试获取共享锁
        int r = tryAcquireShared(arg);
        // 如果在这里成功获取共享锁,会进入共享锁唤醒逻辑
        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);
  }
}

看到上面的代码,是不是有一种熟悉的感觉, 同样是采用了自旋机制,在线程挂起之前,不断地循环尝试获取锁,不同的是,一旦获取共享锁,会调用 setHeadAndPropagate 方法同时唤醒后继节点,实现共享模式 ,下面是唤醒后继节点代码逻辑:

private void setHeadAndPropagate(Node node, int propagate) {
  // 头节点
  Node h = head; 
  // 设置当前节点为新的头节点
  // 这里不需要加锁操作,因为获取共享锁后,会从FIFO队列中依次唤醒队列,并不会产生并发安全问题
  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();
  }
}

该方法主要做了两个重要的步骤:

  1. 将当前节点设置为新的头节点,这点很重要,这意味着当前节点的前置节点(旧头节点)已经获取共享锁了,从队列中去除;
  2. 调用 doReleaseShared 方法,它会调用 unparkSuccessor 方法唤醒后继节点。

释放锁

public final boolean releaseShared(int arg) {
  // 由用户自行实现释放锁条件
  if (tryReleaseShared(arg)) {
    // 执行释放锁
    doReleaseShared();
    return true;
  }
  return false;
}

下面是释放锁逻辑:

private void doReleaseShared() {
  for (;;) {
    // 从头节点开始执行唤醒操作
    // 这里需要注意,如果从setHeadAndPropagate方法调用该方法,那么这里的head是新的头节点
    Node h = head;
    if (h != null && h != tail) {
      int ws = h.waitStatus;
      //表示后继节点需要被唤醒
      if (ws == Node.SIGNAL) {
        // 初始化节点状态
        //这里需要CAS原子操作,因为setHeadAndPropagate和releaseShared这两个方法都会顶用doReleaseShared,避免多次unpark唤醒操作
        if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
          // 如果初始化节点状态失败,继续循环执行
          continue;            // loop to recheck cases
        // 执行唤醒操作
        unparkSuccessor(h);
      }
      //如果后继节点暂时不需要唤醒,那么当前头节点状态更新为PROPAGATE,确保后续可以传递给后继节点
      else if (ws == 0 &&
               !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
        continue;                // loop on failed CAS
    }
    // 如果在唤醒的过程中头节点没有更改,退出循环
    // 这里防止其它线程又设置了头节点,说明其它线程获取了共享锁,会继续循环操作
    if (h == head)                   // loop if head changed
      break;
  }
}

共享锁的释放锁逻辑比独占锁的释放锁逻辑稍微复杂,原因是共享锁需要释放队列中所有共享类型的节点,因此需要循环操作,由于释放锁过程中会涉及多个地方修改节点状态,此时需要 CAS 原子操作来并发安全。

获取共享锁流程图:

UBzAr26.jpg!web

总结

更独占锁相比,从流程图也可看出,共享锁的主要特征是当有一个线程获取到锁之后,那么它就会依次唤醒等待队列中可以跟它共享的节点,当然这些节点也是共享锁类型。

uYZ7zyu.jpg!web

© 著作权归作者所有

上一篇: 从源码的角度解析Mybatis的会话机制

下一篇: Java并发之AQS源码分析(一)

MF3YjuY.jpg!web

Java科代表

粉丝 16

博文 14

码字总数 29811

作品 0

广州

程序员

提问

相关文章 最新文章

CountDownLatch源码解析

CountDownLatch 相比ReentranceLock,CountDownLatch的流程还是相对比较简单的,CountDownLatch也是基于AQS,它是AQS的共享功能的一个实现。 下面从源代码的实现上详解CountDownLatch。 1、C...

maxam0128

2018/01/23

0

0

rYvIBnu.png!web
Java 并发编程源码解析汇总篇

java并发编程,内存模型 java并发编程,volatile内存实现和原理 Java并发编程,并发基础 Java 并发编程,线程池(ThreadPoolExecutor)源码解析 Java并发编程,Executor 框架介绍 Java并发编...

郑加威

2018/12/23

0

0

【Java并发专题】27篇文章详细总结Java并发基础知识

努力的意义,就是,在以后的日子里,放眼望去全是自己喜欢的人和事! github:https://github.com/CL0610/Java-concurrency,欢迎题issue和Pull request。所有的文档都是自己亲自码的,如果觉...

你听___

2018/05/06

0

0

mUFnuqY.png!web
还没用上 JDK 11吧,JDK 12 早期访问构建版使用

JDK 更新速度快的飞起,JDK 12 早期访问构建版已发布,你现在用到了第几版本? 本周Java的最大新闻可能是JDK 11的正式发布。不过在 6 月底,JDK 11 就已经进入了 Rampdown Phase One 阶段,这...

关注公众号_搜云库_每天更新

2018/09/27

0

1

111 多线程JUC包下代码分析

Java多线程系列目录(共43篇) AtomicLongFieldUpdater:通过反射+CAS实现对传入对象的指定long字段实现类似AtomicLong的操作 http://www.cnblogs.com/skywang12345/p/javathreadscategory.ht...

素雷

2017/10/31

0

0

没有更多内容

加载失败,请刷新页面

加载更多
基于 Elasticsearch 的 Zipkin 统计

最近将 Zipkin 的底层存储切换到了 Elasticsearch,相比 Cassandra,Elasticsearch 拥有更加灵活的查询和聚合方式,所以可以完成一些之前做不到的自定义统计,在此记录一下。 存储结构 Zipk...

xiaomin0322

13分钟前

0

0

spring statemachine的企业可用级开发指南6-持久化

目前为止,我们都是从状态流程的开始阶段创建一个状态机,然后一路走下去。但在实际业务中,状态机可能需要在某个环节停留,等待其他业务的触发,然后再继续下面的流程。比如订单,可能在支付...

wphmoon

26分钟前

1

0

ajax是什么?如何创建一个ajax?

ajax是什么 ajax(asynchronous javascript and xml)主要用来实现客户端与服务器端的异步通信,实现页面的局部刷新。 如何创建一个ajax 创建XMLHttpRequest XMLHttpRequest用于在后台与服务...

祖达

32分钟前

1

0

使用ApplicationContext方式得到bean的值

@Componentpublic class SpringBeanUtil implements ApplicationContextAware { private static ApplicationContext applicationContext; public void setApplicationContext(......

小爪进击者

34分钟前

1

0

背包问题概述(Lintcode- 562.Backpack IV问题解决)

什么是背包问题 背包问题(Knapsack problem)是一种组合优化的NP完全问题。问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的...

AI考拉

40分钟前

3

0

没有更多内容

加载失败,请刷新页面

加载更多

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK