AQS(AbstractQueuedSynchronizer)介绍-01
1.概述
AQS( AbstractQueuedSynchronizer ) 是一个用于构建锁和同步器的框架,许多同步器都可以通过AQS很容易并且高效地构造出来。如: ReentrantLock 和 Semaphore都是基于AQS构建的,还包括CountDownLatch、ReentrantReadWriteLock、SynchronousQueue和FutureTask。
AQS解决了在实现同步器时涉及的大量细节问题,例如等待线程采用FIFO队列操作顺序。在不同的同步器中还可以定义一些灵活的标准来判断某个线程是应该通过还是需要等待。本文通过介绍AQS相关API以及juc包下相关实现类对其进行介绍
2.原理
使用volatile的整形变量state用来表示当前是否可以获取锁,如在某些非共享锁里,state=1 则代表当前锁已经被占有,此时如果有线程来请求锁时,则会进入AQS里维持的CLH队列(FIFO)里,且使用自旋的方式不断的尝试获取锁。自定义的同步器在实现时,只需要定义state变量获取与释放的规则,其他细节在AQS里已经实现。
3.独占模式
acquire(int arg) 独占锁获取state资源时调用的方法。
- tryAcquire(子类实现)获取资源,如果获取成功则返回true,不成功返回false,进入下一步
- addWaiter(...),实例化一个排他节点,入队,返回该实例的节点
- 调用acqireQueue(..) 方法 自旋方式获取资源,成功则false,失败返回false。false代表中断的响应,如果在队列里是不会响应中断的,获取资源时才响应。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire(int arg) 需要实现类去实现的一个独占方式获取资源时的方法,成功返回true,失败为false(简而言之,通过state状态,判断当前资源是否可用)
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
addWaiter(Node mode) 获取资源失败后,将当前线程和模式封装到Node节点,并添加到队列里
private Node addWaiter(Node mode) {
//实例化Node节点
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// 尾插法快速插入到链表末尾
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 如果尾部节点为空,则初始化一次,再使用尾插法插入
enq(node);
return node;
}
acquireQueued(final Node node, int arg) 该方法主要功能是通过自旋不断尝试获取资源,在这个过程中,一旦没有获取资源成功则 park 当前线程进入等待唤醒的状态,同时检查当前线程是否中断,如果被设置了中断的标记,则 将interrupted 设置为true,在获取资源成功的时候,去响应中断
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
//判断当前节点是否被通知中断
boolean interrupted = false;
//自旋方式,不断尝试获取资源
for (;;) {
final Node p = node.predecessor();
//如果上一个节点是Head节点,则尝试获取资源
if (p == head && tryAcquire(arg)) {
//获取资源成功,则将当前节点设为Head节点
setHead(node);
//当前节点获取资源成功,那么之前的Head节点需要释放,因此将之前Head相关引用设为NULL,帮助GC回收
p.next = null; // help GC
failed = false;
//返回中断过程是否中断过
return interrupted;
}
//递归判断当前节点的上一个节点的waitStatus是否有效
//park 当前Node节点的线程(wait当前线程),检查是否中断
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire(Node pred, Node node) 此方法主要用于检查waitStatus的状态,看看自己是否真的可以去休息了(进入waiting状态),万一队列前边的线程都放弃了,则丢弃引用,让GC回收。
Node节点中维持着waitStatus这个字段代表的含义如下:
/** waitStatus value to indicate thread has cancelled */
//值为1,在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该Node的结点,其结点的waitStatus为CANCELLED,即结束状态,进入该状态后的结点将不会再变化。
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
//值为-1,被标识为该等待唤醒状态的后继结点,当其前继结点的线程释放了同步锁或被取消,将会通知该后继结点的线程执行。说白了,
//就是处于唤醒状态,只要前继结点释放锁,就会通知标识为SIGNAL状态的后继结点的线程执行。
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
//值为-2,与Condition相关,该标识的结点处于等待队列中,结点的线程等待在Condition上,当其他线程调用了Condition的signal()
// 方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
//值为-3,与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。
static final int PROPAGATE = -3;
//0 代表为初始状态
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//判断前一个节点的waitStatus 是否小于0,如果大于0,则该节点已经被取消
int ws = pred.waitStatus;
//如果为等待通知,则表示等待unpark(等待唤醒)
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
//如果上一个节点被取消掉(大于0)则跳过,循环的找上一节点为未取消的(小于0)
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
//如果当前节点的waitStatus 小于0,但不等于SIGNAL,则设置为SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
parkAndCheckInterrupt() 让线程去休息,真正进入等待状态。
private final boolean parkAndCheckInterrupt() {
//调用park()使线程进入waiting状态
LockSupport.park(this);
//如果被唤醒,查看自己是不是被中断的
return Thread.interrupted();
}
park()会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:1)被unpark();2)被interrupt()。需要注意的是,Thread.interrupted()会清除当前线程的中断标记位。
独占模式的大致流程知道了后,用流程图来总结下(贴上源码)
1 public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
- 调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;
- 没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
- acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
- 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
release(int arg) 独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。这也正是unlock()的语义,当然不仅仅只限于unlock()。下面是release()的源码:
public final boolean release(int arg) {
//子类实现释放资源,释放成功则返回true,失败则false
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
//unpart 下一个等待的节点
unparkSuccessor(h);
return true;
}
return false;
}
tryRelease(int arg) 需要子类实现的获取资源成功的方法,用作父类根据子类释放是否成功作为判断依据
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
unparkSuccessor(Node node) 唤醒队列中的下一个未取消的线程
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0) //设置当前线程的waitStatus为0
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next; //找到head节点的下一个需要唤醒的节点,如果下一个节点被取消了(>0),则从tail往前找
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//unpart 唤醒线程
if (s != null)
LockSupport.unpark(s.thread);
}
4.共享模式
acquireShared(int) 此方法是共享模式下线程获取共享资源的顶层入口。它会获取指定量的资源,获取成功则直接返回,获取失败则进入等待队列,直到获取到资源为止,整个过程忽略中断。下面是acquireShared()的源码:
public final void acquireShared(int arg) {
//子类实现获取共享资源,成功则返回大于0的整数
if (tryAcquireShared(arg) < 0)
//获取资源失败,进入队列等待
doAcquireShared(arg);
}
doAcquireShared(int) 此方法用于将当前线程加入等待队列尾部休息,直到其他线程释放资源唤醒自己,自己成功拿到相应量的资源后才返回。下面是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) {//如果到head的下一个,因为head是拿到资源的线程,此时node被唤醒,很可能是head用完资源来唤醒自己的
int r = tryAcquireShared(arg);//尝试获取资源
if (r >= 0) {//成功
setHeadAndPropagate(node, r);//将head指向自己,还有剩余资源可以再唤醒之后的线程
p.next = null; // help GC
if (interrupted)//如果等待过程中被打断过,此时将中断补上。
selfInterrupt();
failed = false;
return;
}
} //判断状态,寻找安全点,进入waiting状态,等着被unpark()或interrupt()
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
与acquireQueued大致相同。只不过这里将补中断的selfInterrupt()放到doAcquireShared()里了,而独占模式是放到acquireQueued()之外。
setHeadAndPropagate(Node, int) 此方法在setHead()的基础上多了一步,就是自己苏醒的同时,如果条件符合(比如还有剩余资源),还会去唤醒后继结点,毕竟是共享模式!
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
setHead(node);//head指向自己
//如果还有剩余量,继续唤醒下一个邻居线程
if (propagate > 0 || h == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
小结
OK,至此,acquireShared()也要告一段落了。让我们再梳理一下它的流程:
- tryAcquireShared()尝试获取资源,成功则直接返回;
- 失败则通过doAcquireShared()进入等待队列park(),直到被unpark()/interrupt()并成功获取到资源才返回。整个等待过程也是忽略中断的。
其实跟acquire()的流程大同小异,只不过多了个自己拿到资源后,还会去唤醒后继队友的操作(这才是共享嘛)。
releaseShared() 上一小节已经把acquireShared()说完了,这一小节就来讲讲它的反操作releaseShared()吧。此方法是共享模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果成功释放且允许唤醒等待线程,它会唤醒等待队列里的其他线程来获取资源。下面是releaseShared()的源码:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {//尝试释放资源
doReleaseShared();//唤醒后继结点
return true;
}
return false;
}
此方法的流程也比较简单,一句话:释放掉资源后,唤醒后继。跟独占模式下的release()相似,但有一点稍微需要注意:独占模式下的tryRelease()在完全释放掉资源(state=0)后,才会返回true去唤醒其他线程,这主要是基于独占下可重入的考量;而共享模式下的releaseShared()则没有这种要求,共享模式实质就是控制一定量的线程并发执行,那么拥有资源的线程在释放掉部分资源时就可以唤醒后继等待结点。ReentrantReadWriteLock读锁的tryReleaseShared()只有在完全释放掉资源(state=0)才返回true,所以自定义同步器可以根据需要决定tryReleaseShared()的返回值。
doReleaseShared() 此方法主要用于唤醒后继。下面是它的源码:
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
unparkSuccessor(h);//唤醒后继
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head)// head发生变化
break;
}
}
AQS(AbstractQueuedSynchronizer)介绍-01的更多相关文章
- 4.锁--Synchronizer Framework Base Class—AbstractQueuedSynchronizer介绍
1. AQS简单介绍 AQS是Java并发类库的基础.其提供了一个基于FIFO队列,可以用于构建锁或者其它相关同步装置的基础框架.该同步器(下面简称同步器)利用了一个int来表示状态,期望它可以成为实 ...
- 高并发第十一弹:J.U.C -AQS(AbstractQueuedSynchronizer) 组件:Lock,ReentrantLock,ReentrantReadWriteLock,StampedLock
既然说到J.U.C 的AQS(AbstractQueuedSynchronizer) 不说 Lock 是不可能的.不过实话来说,一般 JKD8 以后我一般都不用Lock了.毕竟sychronize ...
- 从ReentrantLock看AQS (AbstractQueuedSynchronizer) 运行流程
从ReentrantLock看AQS (AbstractQueuedSynchronizer) 运行流程 概述 本文将以ReentrantLock为例来讲解AbstractQueuedSynchron ...
- vue项目搭建介绍01
目录 vue项目搭建介绍01 vue 项目框架环境搭建: 创建项目: vue 项目创建流程: vue项目搭建介绍01 vue 项目框架环境搭建: vue 项目框架: vue django(类似)(vu ...
- 082 01 Android 零基础入门 02 Java面向对象 01 Java面向对象基础 02 构造方法介绍 01 构造方法-无参构造方法
082 01 Android 零基础入门 02 Java面向对象 01 Java面向对象基础 02 构造方法介绍 01 构造方法-无参构造方法 本文知识点:构造方法-无参构造方法 说明:因为时间紧张, ...
- 高并发第十单:J.U.C AQS(AbstractQueuedSynchronizer) 组件:CountDownLatch. CyclicBarrier .Semaphore
这里有一篇介绍AQS的文章 非常好: Java并发之AQS详解 AQS全名:AbstractQueuedSynchronizer,是并发容器J.U.C(java.lang.concurrent)下lo ...
- 5. AQS(AbstractQueuedSynchronizer)抽象的队列式的同步器
5.1 AbstractQueuedSynchronizer里面的设计模式--模板模式 模板模式:父类定义好了算法的框架,第一步做什么第二步做什么,同时把某些步骤的实现延迟到子类去实现. 5.1.1 ...
- AQS(AbstractQueuedSynchronizer)应用案例-02
1.概述 通过对AQS源码的熟悉,我们可以通过实现AQS实现自定义的锁来加深认识. 2.实现 1.首先我们确定目标是实现一个独占模式的锁,当其中一个线程获得资源时,其他线程再来请求,让它进入队列进行公 ...
- AQS(AbstractQueuedSynchronizer)解析
AbstractQueuedSynchronizer是JUC包下的一个重要的类,JUC下的关于锁相关的类(如:ReentrantLock)等大部分是以此为基础实现的.那么我们就来分析一下AQS的原理. ...
随机推荐
- form表单验证失败,阻止表单提交
form表单验证失败,阻止表单提交 效果演示: 贴上完整代码: <!DOCTYPE html> <html lang="en"> <head> ...
- 仿新浪游戏频道js多栏目全屏下拉菜单导航条
仿新浪游戏频道js多栏目全屏下拉菜单导航条,新浪,游戏频道,js组件,多栏目,全屏下拉,下拉菜单,导航条.代码下载地址:http://www.huiyi8.com/sc/26765.html更多请访问 ...
- Delphi中TList类应用
在DELPHI中指针最常见的就是和类TLIST结合起来使用.下面是一个很简单的例子,希望对这个例子的分析能让大家对使用TLIST类有一个简单的认识. 代码的功能是使用指针和Tlist来生成一个牌串,并 ...
- Guid 使用记录
Guid 使用记录; Guid 数据不能设为null 用 00000000-0000-0000-0000-000000000000 代替默认值. 实体新建时,可以赋值为 Guid.Empty 也就是 ...
- Unity3D中Mathf数学运算函数总结
引入: 看到一个案例注意到函数Mathf.SmoothDamp的使用,游戏中用于做相机的缓冲跟踪和boss直升机跟踪士兵.该函数是Unity3D中Mathf数学运算函数中的一个.一些游戏使用了smoo ...
- Oracle学习笔记_04_多表查询
一.概念: 1.多表连接有以下几种分法: (1)内连接 vs 外连接 (左.右.满) (2)等值连接 vs 不等值连接 (3)非自连 ...
- 谈MicroMessageTest的开始创建
一开始,创建一个可以看到的jsp前端页面. 只不过不是用纯jsp页面访问,而是用Servlet doGet跳转至jsp页面,req.getRequestDispatcher(jsp页面的全称 还是全地 ...
- hihocoder-1285 智力竞赛(区间dp)
智力竞赛 时间限制:5000ms 单点时限:1000ms 内存限制:256MB 描述 小Hi.小Ho还有被小Hi强拉来的小Z,准备组队参加一个智力竞赛.竞赛采用过关制,共计N个关卡.在第i个关卡中,小 ...
- AtCoder Grand Contest 014 题解
A - Cookie Exchanges 模拟 Problem Statement Takahashi, Aoki and Snuke love cookies. They have A, B and ...
- android开发中 解决服务器端解析MySql数据时中文显示乱码的情况
首先,还是确认自己MySql账户和密码 1.示例 账户:root 密码:123456 有三个字段 分别是_id .username(插入有中文数据).password 1)首先我们知道 ...