Java并发包源码学习系列:AQS共享模式获取与释放资源

往期回顾:

上一篇文章介绍了AQS内置队列节点的出队入队操作,以及独占式获取共享资源与释放资源的详细流程,为了结构完整,本篇继续以AQS的角度介绍另外一种:共享模式获取与释放资源的细节,本篇暂不分析具体子类如ReentrantLock、ReentrantReadWriteLock的实现,之后会陆续补充。

独占式获取资源

友情提示:本篇文章着重介绍共享模式获取和释放资源的特点,许多代码实现上面和共享式和独占式其实逻辑差不多,为了清晰对比,这边会将独占式的部分核心代码粘贴过来,注意理解共享式和独占式存在差异的地方。详细解析可戳:Java并发包源码学习系列:CLH同步队列及同步资源获取与释放

void acquire(int arg)

    public final void acquire(int arg) {
if (!tryAcquire(arg) && // tryAcquire由子类实现,表示获取锁,如果成功,这个方法直接返回了
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 如果获取失败,执行
selfInterrupt();
}

boolean acquireQueued(Node, int)

    // 这个方法如果返回true,代码将进入selfInterrupt()
final boolean acquireQueued(final Node node, int arg) {
// 注意默认为true
boolean failed = true;
try {
// 是否中断
boolean interrupted = false;
// 自旋,即死循环
for (;;) {
// 得到node的前驱节点
final Node p = node.predecessor();
// 我们知道head是虚拟的头节点,p==head表示如果node为阻塞队列的第一个真实节点
// 就执行tryAcquire逻辑,这里tryAcquire也需要由子类实现
if (p == head && tryAcquire(arg)) {
// tryAcquire获取成功走到这,执行setHead出队操作
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 走到这有两种情况 1.node不是第一个节点 2.tryAcquire争夺锁失败了
// 这里就判断 如果当前线程争锁失败,是否需要挂起当前这个线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 死循环退出,只有tryAcquire获取锁失败的时候failed才为true
if (failed)
cancelAcquire(node);
}
}

独占式释放资源

boolean release(int arg)

    public final boolean release(int arg) {
if (tryRelease(arg)) { // 子类实现tryRelease方法
// 获得当前head
Node h = head;
// head不为null并且head的等待状态不为0
if (h != null && h.waitStatus != 0)
// 唤醒下一个可以被唤醒的线程,不一定是next哦
unparkSuccessor(h);
return true;
}
return false;
}

void unparkSuccessor(Node node)

	private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
// 如果node的waitStatus<0为signal,CAS修改为0
// 将 head 节点的 ws 改成 0,清除信号。表示,他已经释放过了。不能重复释放。
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0); /*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
// 唤醒后继节点,但是有可能后继节点取消了等待 即 waitStatus == 1
Node s = node.next;
// 如果后继节点为空或者它已经放弃锁了
if (s == null || s.waitStatus > 0) {
s = null;
// 从队尾往前找,找到没有没取消的所有节点排在最前面的【直到t为null或t==node才退出循环嘛】
for (Node t = tail; t != null && t != node; t = t.prev)
// 如果>0表示节点被取消了,就一直向前找呗,找到之后不会return,还会一直向前
if (t.waitStatus <= 0)
s = t;
}
// 如果后继节点存在且没有被取消,会走到这,直接唤醒后继节点即可
if (s != null)
LockSupport.unpark(s.thread);
}

共享式获取资源

void acquireShared(int arg)

    public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0) //子类实现
doAcquireShared(arg);
}
  • tryAcquireShared(int)是AQS提供给子类实现的钩子方法,子类可以自定义实现共享式获取资源的方式,获取状态失败返回小于0,返回零值表示被独占方式获取,返回正值表示共享方式获取。
  • 如果获取失败,则进入doAcquireShared(arg);的逻辑。

void doAcquireShared(int arg)

注意这里和独占式获取资源acquireQueued的区别。

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

我们可以看到有几个存在差异的地方:

  1. 在共享式获取资源失败的时候,会包装成SHARED模式的节点入队。
  2. 如果前驱节点为head,则使用tryAcquireShared方法尝试获取同步状态,这个方法由子类实现。
  3. 如果获取成功r>=0,这时调用setHeadAndPropagate(node, r),该方法首先会设置新的首节点,将第一个节点出队,接着会不断唤醒下一个共享模式节点,实现同步状态被多个线程共享获取。

接下来我们着重看下setHeadAndPropagate方法。

void setHeadAndPropagate(Node node, int propagate)

    private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
// 节点出队,设置node为新的head
setHead(node);
/*
* Try to signal next queued node if:
* Propagation was indicated by caller,
* or was recorded (as h.waitStatus either before
* or after setHead) by a previous operation
* (note: this uses sign-check of waitStatus because
* PROPAGATE status may transition to SIGNAL.)
* and
* The next node is waiting in shared mode,
* or we don't know, because it appears null
*
* The conservatism in both of these checks may cause
* unnecessary wake-ups, but only when there are multiple
* racing acquires/releases, so most need signals now or soon
* anyway.
*/
// 这个方法进来的时候propagate>=0
// propagate>0表示同步状态还可以被后面的节点获取
// h指向原先的head节点,之后h = head,h表示新的head节点
// h.waitStatus<0表示该节点后面还有节点需要被唤醒
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
// 获取下一个节点
Node s = node.next;
// 没有下一个节点或下一个节点为共享式获取状态
if (s == null || s.isShared())
// 唤醒后续的共享式获取同步状态的节点
doReleaseShared();
}
}
  • 先记录一下原来的头节点,然后设置node为新的头节点。
  • 原先的头节点或新的头节点等待状态是propagate或signal,可以继续向下唤醒。
  • 如果判断下个节点为shared节点,调用共享式释放资源方法唤醒后续节点。

共享式释放资源

boolean releaseShared(int arg)

    public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { // 子类实现
doReleaseShared();
return true;
}
return false;
}

doReleaseShared()

可以发现共享模式下,无论是获取资源还是释放资源都调用了doReleaseShared方法,可见该方法是共享模式释放资源唤醒节点的核心方法,主要功能是唤醒下一个线程或者设置传播状态

后继线程被唤醒后,会尝试获取共享锁,如果成功之后,则又会调用setHeadAndPropagate,将唤醒传播下去。这个方法的作用是保障在acquire和release存在竞争的情况下,保证队列中处于等待状态的节点能够有办法被唤醒。

    private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
// 自旋
for (;;) {
Node h = head;
// 队列已经初始化且至少有一个节点
if (h != null && h != tail) {
int ws = h.waitStatus;
// 无论是独占还是共享,只有节点的ws为signal的时候,才会在释放的时候,唤醒后面的节点
if (ws == Node.SIGNAL) {
// cas将ws设置为0,设置失败,将会继续从循环开始
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
// 唤醒后继节点,unparkSuccessor这个方法上面已经解析过
unparkSuccessor(h);
}
// 如果ws为0,则更新状态为propagate,
// 之后setHeadAndPropagate读到ws<0的时候,会继续唤醒后面节点
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 如果head在这个过程中被更改了,会继续自旋
if (h == head) // loop if head changed
break;
}
}

该方法在 head 节点存在后继节点的情况下,做了两件事情:

  1. 如果 head 节点等待状态为 SIGNAL,则将 head 节点状态设为 0,并唤醒后继未取消节点。

  2. 如果 head 节点等待状态为 0,则将 head 节点状态设为 PROPAGATE,保证唤醒能够正常传播下去。

设置PROPAGATE的作用:PROPAGATE状态用在[setHeadAndPropagate](#void setHeadAndPropagate(Node node, int propagate)) ,当头节点状态被设为 PROPAGATE 后,后继节点成为新的头结点后。若 propagate > 0 条件不成立,则根据条件h.waitStatus < 0成立与否,来决定是否唤醒后继节点,即向后传播唤醒动作。

引入PROPAGATE是为了解决什么问题?

AbstractQueuedSynchronizer源码解读,强烈建议阅读这篇博客。

独占式和共享式的区别总结

共享式获取与独占式获取最大的区别就是同一时刻能否有多个线程同时获取到同步状态。

  • 共享式访问资源时,同一时刻其他共享式的访问会被允许。
  • 独占式访问资源时,同一时刻其他访问均被阻塞。

AQS都提供了子类实现的钩子方法,独占式的代表方法有:tryAcquire和tryRelease以及isHeldExclusively方法,共享式的代表方法有:tryAcquireShared和tryReleaseShared方法。

AQS中获取操作和释放操作的标准形式:

boolean acquire() throws InterruptedException{
while( 当前状态不允许获取操作 ){
if( 需要阻塞获取请求){
如果当前线程不在队列中,则将其插入队列
阻塞当前线程
}else{
返回失败
}
}
可能更新同步器的状态
如果线程位于队列中,则将其移除队列
返回成功
} void release(){
更新同步器的状态
if( 新的状态允许某个被阻塞的线程获取成功 ){
解除队列中一个或多个线程的阻塞状态
}
}

图源:《并发编程的艺术》下图是独占式同步状态获取的流程

当某个线程争夺同步资源失败之后,他们都会将线程包装为节点,并加入CLH同步队列的队尾,并保持自旋,一个是addWaiter(Node.EXCLUSIVE),一个是addWaiter(Node.EXCLUSIVE)

同步队列中的线程在自旋时会判断其前驱节点是否为首节点,如果是首节点node.predecessor() == head,他们都会尝试获取同步状态,只不过:

  • 独占式获取状态成功后,只会出队一个节点。
  • 共享式获取状态成功后,除了出队一个节点,还会唤醒后面的节点。

线程执行完逻辑之后,他们都会释放同步状态,释放之后将会unparkSuccessor(h)唤醒其后可被唤醒的某个后继节点。

参考阅读

Java并发包源码学习系列:AQS共享式与独占式获取与释放资源的区别的更多相关文章

  1. Java并发包源码学习系列:CLH同步队列及同步资源获取与释放

    目录 本篇学习目标 CLH队列的结构 资源获取 入队Node addWaiter(Node mode) 不断尝试Node enq(final Node node) boolean acquireQue ...

  2. Java并发包源码学习系列:ReentrantLock可重入独占锁详解

    目录 基本用法介绍 继承体系 构造方法 state状态表示 获取锁 void lock()方法 NonfairSync FairSync 公平与非公平策略的差异 void lockInterrupti ...

  3. Java并发包源码学习系列:ReentrantReadWriteLock读写锁解析

    目录 ReadWriteLock读写锁概述 读写锁案例 ReentrantReadWriteLock架构总览 Sync重要字段及内部类表示 写锁的获取 void lock() boolean writ ...

  4. Java并发包源码学习系列:详解Condition条件队列、signal和await

    目录 Condition接口 AQS条件变量的支持之ConditionObject内部类 回顾AQS中的Node void await() 添加到条件队列 Node addConditionWaite ...

  5. Java并发包源码学习系列:挂起与唤醒线程LockSupport工具类

    目录 LockSupport概述 park与unpark相关方法 中断演示 blocker的作用 测试无blocker 测试带blocker JDK提供的demo 总结 参考阅读 系列传送门: Jav ...

  6. Java并发包源码学习系列:JDK1.8的ConcurrentHashMap源码解析

    目录 为什么要使用ConcurrentHashMap? ConcurrentHashMap的结构特点 Java8之前 Java8之后 基本常量 重要成员变量 构造方法 tableSizeFor put ...

  7. Java并发包源码学习系列:阻塞队列BlockingQueue及实现原理分析

    目录 本篇要点 什么是阻塞队列 阻塞队列提供的方法 阻塞队列的七种实现 TransferQueue和BlockingQueue的区别 1.ArrayBlockingQueue 2.LinkedBloc ...

  8. Java并发包源码学习系列:阻塞队列实现之ArrayBlockingQueue源码解析

    目录 ArrayBlockingQueue概述 类图结构及重要字段 构造器 出队和入队操作 入队enqueue 出队dequeue 阻塞式操作 E take() 阻塞式获取 void put(E e) ...

  9. Java并发包源码学习系列:阻塞队列实现之LinkedBlockingQueue源码解析

    目录 LinkedBlockingQueue概述 类图结构及重要字段 构造器 出队和入队操作 入队enqueue 出队dequeue 阻塞式操作 E take() 阻塞式获取 void put(E e ...

随机推荐

  1. 通过CSS绘制五星红旗

    任务要求: 1.创建一个div作为红旗旗面,用CSS控制其比例宽高比为3:2,背景为红色. 2.再创建五个小的div,用CSS控制其大小和位置. 3.用CSS同时控制每个小div的大小.边框和位置,同 ...

  2. P4317 花神的数论题,关于luogu题解粉兔做法的理解

    link 题意 设 \(\text{sum}(i)\) 表示 \(i\) 的二进制表示中 \(1\) 的个数.给出一个正整数 \(N\) ,求 \(\prod_{i=1}^{N}\text{sum}( ...

  3. 解压版mysql+免破解版Navicat,好用!

    解压版mysql安装流程 获取mysql压缩包 获取地址: 链接:https://pan.baidu.com/s/1HqdFDQn_6ccPM0gOftApIg 提取码:n19t 获取压缩包后可安装压 ...

  4. Jmeter(9)常用定时器

    测试计划中元件的执行顺序依次为: 配置元件--逻辑控制器--前置处理器--定时器--取样器--后置处理器--断言--监听器 一.定时器作用域 1.定时器是在每个取样器之前执行的,无论定时器是在取样器之 ...

  5. 聊聊Spring的FactoryBean其实没那么难

    前言 谈到Spring的FactoryBean,就会知道Spring中经典的面试题:FactoryBean和BeanFactory的区别.我们这里就简单概括下: . BeanFactory是接口,提供 ...

  6. Exception in thread "main" java.lang.NoSuchMethodError: scala.collection.immutable.HashSet$.empty()Lscala/collection/immutable/HashSet;

    注意spark的Scala版本和java版本 修改后为官方指定的版本正常运行 Error:scalac: Error: object FloatRef does not have a member c ...

  7. pandas的学习3-设置值

    import pandas as pd import numpy as np # 我们可以根据自己的需求, 用 pandas 进行更改数据里面的值, 或者加上一些空的,或者有数值的列. # 首先建立了 ...

  8. 微服务之服务治理_Eureka

    首先需要明确,不管是什么事物需要"治理",那一定是该事物存在一定问题.比如环境治理.那么服务,或者说微服务为什么需要治理?对于服务来说,如果它承担的业务职责简单,那其实治理的必要性 ...

  9. 关于ABAP和JSON互相转换

    关于ABAP数据结构和JSON格式转换,需要用到标准的类/UI2/CL_JSON一下两个方法, DESERIALIZE是把JSON格式转换成ABAP数据结构,SERIALIZE是把ABAP数据结构转换 ...

  10. Linux-centos-64bit安装MySQL

    1.下载mysql安装包到 /usr/local/soft [root@VM_0_9_centos ~]# cd /usr/local/soft[root@VM_0_9_centos soft]# w ...