大纲:

  1. AbstractQueuedSynchronizer简介
  2. aqs基本结构
  3. aqs应用-ReentrantLock.lock
  4. aqs应用-ReentrantLock.unlock
  5. aqs应用-Semaphore.acquire

一、AbstractQueuedSynchronizer简介

AbstractQueuedSynchronizer(抽象队列同步器)简介:AbstractQueuedSynchronizer以下简称(aqs)是一个基于先进先出队列,用于构建锁及其他同步装置的基础框架。子类通过继承aqs实现同步的需求。

二、aqs基本结构

aqs的数据结构是一个双向链表,aqs的主要成员变量是头尾节点,还有一个state(线程的同步状态)

    //头尾节点
private volatile Node head;
private volatile Node tail;
//同步状态
private volatile int state;

节点是一个aqs类中的嵌套类,看下节点的结构:

static final class Node {
//节点类型
static final Node EXCLUSIVE = null;
static final Node SHARED = new Node(); static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
//前后节点
volatile Node prev;
volatile Node next;
//节点中存储的线程
volatile Thread thread;
Node nextWaite;
//等待状态CANCELLED/SIGNAL/CONDITION/PROPAGATE
volatile int waitStatus;
// Used to establish initial head or SHARED marker
Node() {} // Used by Condition
public Node(Thread thread, int waitStatus) {
this.thread = thread;
this.waitStatus = waitStatus;
}
// Used by addWaiter
public Node(Thread thread, Node nextWaite) {
this.thread = thread;
this.nextWaite = nextWaite;
}
}

没有获取到资源的线程被包装成为一个节点,每个节点有一个等待状态,节点中存储着节点的前驱与后继节点。

三、aqs应用-ReentrantLock.lock

重入锁ReentrantLock的lock方法就是利用了aqs做的同步。

tip:阅读aqs首先要明白cas是什么https://www.cnblogs.com/liuboyuan/p/10449503.html

首先ReentrantLock类中有一个抽象的嵌套类Sync:

abstract static class Sync extends AbstractQueuedSynchronizer {
abstract void lock();
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//获取同步状态
int c = getState();
if (c == 0) { //当前没有线程获得锁
if (compareAndSetState(0, acquires)) { //通过cas将state修改成1表示当前线程获得了锁
//记录持有锁的线程,aqs父类AbstractOwnableSynchronizer的方法
setExclusiveOwnerThread(current);
//获取锁成功
return true;
}
}
else if (current == getExclusiveOwnerThread()) { //当前线程已经获取到了锁
//这里就是重入,又一次进入了同一个锁需要同步的代码
// state+1
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//获取锁失败
return false;
}
}

sync的子类-非公平锁的实现(还有公平锁的实现,原理类似本文不做讨论)

static final class NonfairSync extends Sync {
//非公平锁的lock方法
final void lock() {
//cas修改state状态成功则表示获取锁成功
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//acquire是aqs的方法
acquire(1);
} protected final boolean tryAcquire(int acquires) {
//调用Sync的nonfairTryAcquire方法,获取锁成功返回true失败false
return nonfairTryAcquire(acquires);
}
}

其中acquire是aqs的方法

  //aqs的acquire
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //Node.EXCLUSIVE是独占模式
selfInterrupt();
}

这个acquire是线程同步的核心方法。

首先tryAcquire方法是aqs子类各自完成的,ReentrantLock调用的是sycn中的nonfairTryAcquire。

3.1 addWaiter

这个方法主要是把没有获取到锁的节点插到队尾

private Node addWaiter(Node mode) {
//将当前线程封装成node
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
//队列里原来有值,将node插到队尾
if (pred != null) {
node.prev = tail;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//如果队列空,或者cas插入队尾失败执行enq方法
enq(node);
return node;
} private Node enq(final Node node) {
for (; ; ) {
//获取尾节点
Node t = tail;
if (t == null) {//初始化,队列为空
//初始化一个空的头结点
if (compareAndSetHead(new Node()))
tail = head;
}
//队列里原来有值,将node插到队尾
else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}

这里for循环就是个自旋,由于是并发插入队尾,乐观锁操作cas就有可能失败,所以不断尝试插入队尾直到成功。

3.2 acquireQueued

将节点包装后传入acquireQueued

这个方法首先判断如果节点的前驱节点为头结点并再获取一次锁,如果成功则将该节点设置为头结点。否则进入阻塞阶段。

final boolean acquireQueued(final Node node, int arg) {
boolean failed = true; //获得锁是否失败
try {
boolean interrupted = false;//获取锁过程中被interrupt
for (;;) {
//获取当前节点的前驱节点
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {//当前驱节点是头节点且获取锁成功
//将当前节点设为头结点(头结点表示持有锁的节点)
setHead(node);
p.next = null; // help GC
failed = false;
//这里线程还没被挂起,无法被interrupt
return interrupted;
}
//找到有效前驱节点,设置前驱节点的waitstatus为Node.SIGNAL,之后park挂起线程,等带着被unpark()或interrupt()
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

3.3 shouldParkAfterFailedAcquire

shouldParkAfterFailedAcquire将判断前驱节点状态,并返回是否应该阻塞线程

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//当前节点的前驱的waitStatus
int ws = pred.waitStatus;
//当前驱的waitStatus为Node.SIGNAL表示该线程可以阻塞返回true
if (ws == Node.SIGNAL) {
return true;
}
//当前驱的waitStatus为-1表示前驱节点状态为Node.CANCELLED,跳过前驱,直到找到状态不是Node.CANCELLED的节点,将该节点作为当前节点的前驱
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
}
//将前驱节点的waitStatus改为Node.SIGNAL
else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}

这里首先判断前驱节点的waitState,如果是Node.SIGNAL表示线程可以被挂起了,返回成功;如果>1则表示取消,将先前继续查找,知道找到一个前驱节点状态<=0(正常状态),将这个节点作为前驱节点替换原来的前驱节点;其他情况将waitState修改为Node.SIGNAL,返回失败。等待下次自旋进入shouldParkAfterFailedAcquire,直到把前驱状态改为Node.SIGNAL。

当前驱waitState为Node.SIGNAL时,这个线程就可以安心被挂起了。

3.4 parkAndCheckInterrupt

线程被挂起,等待其他线程唤醒后,检查是否被打断

    private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);//调用本地方法将线程挂起
return Thread.interrupted();//被唤醒时检查是否被打断
}

当parkAndCheckInterrupt返回true则调用Thread.currentThread().interrupt()。

acquire主要流程小结:

  1. tryAcquire:尝试获取锁。成功则返回,失败则有下列流程。
  2. addWaiter:将当前线程包装成一个节点放在队列尾(初始化队列的时候将新建一个空节点在head,再把包装好的节点插在后面)。
  3. acquireQueued:如果当前节点的前驱是头结点则再次尝试获取锁(最后一次挣扎),若成功,则把当前节点设置为头结点,若失败则进入挂起阶段。
  4. shouldParkAfterFailedAcquire将前驱节点的waitState置为Node.SIGNAL。
  5. parkAndCheckInterrupt挂起当前线程,当线程被唤醒检查是否被打断。

四、aqs应用-ReentrantLock.unlock方法

unlock方法的核心是aqs中的release方法

    public final boolean release(int arg) {
if (tryRelease(arg)) {//是否成功释放资源
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);//唤醒队列中的线程
return true;
}
return false;
}
    protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}

4.1 tryRelease方法依然又实现类自己实现,下面是ReentrantLock.Sync的实现

    protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//c>0则有锁重入情况,c==0将释放锁
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);//清除占有锁的线程
}
setState(c);
return free;
}

4.2 unparkSuccessor唤醒节点中的线程

    private void unparkSuccessor(Node node) {
//node为持有锁的线程,也就是当前线程所在节点 或 空队列时新建出来的空节点
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0); //获取下一个节点
Node s = node.next;
//空节点和被取消的节点将寻找下一个有效节点
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)//waitStatus为0 和负数都是有效节点
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);//唤醒这个节点的线程
}

小结:

  1. release流程比较简单,修改state状态,当state状态为0时,唤醒头结点的下一个有效节点中的线程。
  2. 结合acquireQueued来看,被唤醒的线程回到acquireQueued那个for循环里,进入if (p == head && tryAcquire(arg)) 如果p!=head则也会在shouldParkAfterFailedAcquire中调整为第二个节点,因为之前的节点肯定都已经是取消掉的无效节点,到此为止lock和unlock方法就串起来了。

五、aqs应用-Semaphore.acquire方法

Lock的lock方法是调用aqs的acquire方法,该方法是独占模式获取资源的,而Semaphore的acquire调用的是aqs的acquireShared方法,该方法是共享模式获取资源。

    public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0) //tryAcquireShared方法子类各自实现,Semaphore.Sync中实现如下
doAcquireShared(arg);
}

5.1Semaphore.Sync实现的tryAcquireShared

    protected int tryAcquireShared(int acquires //需要获取的资源数) {
for (;;) {
if (hasQueuedPredecessors())
return -1;
int available = getState(); //剩余的资源数
int remaining = available - acquires; //remaining获取资源后剩余的资源数
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}

5.2aqs中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);
}
}

和独占模式的流程如出一辙,入队、挂起 的流程一样,依然是唤醒头结点的下一个有效节点。这里需要注意即使是共享模式,唤醒依然是按照入队顺序来的,但资源数不够第二个节点的获取数时也不会唤醒后续节点(即使资源数满足后续节点的获取数)。

唯一不同的是在资源还有剩余的情况下,会在设置头节点的同时继续唤醒当前节点下一个有效节点

   private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
//propagate > 0(如果剩余的资源数>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();
}
}

小结:

  1. 两种模式获取资源方式差不多,共享模式多了如果资源数>0继续唤醒节点的操作。
  2. 两种模式释放资源的方式也基本一样,这里不再赘述。

java多线程-AbstractQueuedSynchronizer的更多相关文章

  1. Java多线程系列--“JUC锁”03之 公平锁(一)

    概要 本章对“公平锁”的获取锁机制进行介绍(本文的公平锁指的是互斥锁的公平锁),内容包括:基本概念ReentrantLock数据结构参考代码获取公平锁(基于JDK1.7.0_40)一. tryAcqu ...

  2. Java多线程系列--“JUC锁”04之 公平锁(二)

    概要 前面一章,我们学习了“公平锁”获取锁的详细流程:这里,我们再来看看“公平锁”释放锁的过程.内容包括:参考代码释放公平锁(基于JDK1.7.0_40) “公平锁”的获取过程请参考“Java多线程系 ...

  3. Java多线程系列--“JUC锁”01之 框架

    本章,我们介绍锁的架构:后面的章节将会对它们逐个进行分析介绍.目录如下:01. Java多线程系列--“JUC锁”01之 框架02. Java多线程系列--“JUC锁”02之 互斥锁Reentrant ...

  4. java多线程系类:JUC线程池:03之线程池原理(二)(转)

    概要 在前面一章"Java多线程系列--"JUC线程池"02之 线程池原理(一)"中介绍了线程池的数据结构,本章会通过分析线程池的源码,对线程池进行说明.内容包 ...

  5. java多线程系类:JUC锁:01之框架

    本章,我们介绍锁的架构:后面的章节将会对它们逐个进行分析介绍.目录如下:01. Java多线程系列--"JUC锁"01之 框架02. Java多线程系列--"JUC锁&q ...

  6. Java多线程系列--“JUC锁”09之 CountDownLatch原理和示例

    概要 前面对"独占锁"和"共享锁"有了个大致的了解:本章,我们对CountDownLatch进行学习.和ReadWriteLock.ReadLock一样,Cou ...

  7. Java多线程系列--“JUC锁”05之 非公平锁

    概要 前面两章分析了"公平锁的获取和释放机制",这一章开始对“非公平锁”的获取锁/释放锁的过程进行分析.内容包括:参考代码获取非公平锁(基于JDK1.7.0_40)释放非公平锁(基 ...

  8. Java多线程系列--“JUC锁”08之 共享锁和ReentrantReadWriteLock

    概要 Java的JUC(java.util.concurrent)包中的锁包括"独占锁"和"共享锁".在“Java多线程系列--“JUC锁”02之 互斥锁Ree ...

  9. Java多线程系列--“JUC锁”11之 Semaphore信号量的原理和示例

    概要 本章,我们对JUC包中的信号量Semaphore进行学习.内容包括:Semaphore简介Semaphore数据结构Semaphore源码分析(基于JDK1.7.0_40)Semaphore示例 ...

随机推荐

  1. 拓展KMP求回文串

    题目:hdu3613: 题意:有26字母对应的价值,然后给出以个串,把它分成两段字串,如果字串是回文串,串的价值就是每个字符和,不是就为0.求最大价值. 博客 分析:拓展KMP的应用求回文字串. #i ...

  2. 关于springmvc与ajax的交互-开发记录

    每次都栽在这个地方,好衰! 在jsp页面的<form>标签设置了action="请求url" ,button那里用js进行监听,点击触发ajax方法,将前台数据传到后台 ...

  3. 30 System类

    System类代表系统,系统级的很多属性和控制方法都放置在该类的内部.该类位于java.lang包.由于该类的构造方法是private的,所以无法创建该类的对象,也就是无法实例化该类.其内部的成员变量 ...

  4. Delphi之TPersistent类 -----ASSIGN

    Delphi之TPersistent类 TPersistent类 TPersistent类是由TObject直接派生的.凡是由TPersistent派生的对象都能够进行流操作.因为所有的组件都是由TP ...

  5. 微信小程序布局篇

    刚刚接触小程序,小程序与HTML5有一定的差别,小程序就几个标签,而HTML5一大堆标签,还不断更新,但是新增标签功能强大.做一下微信小程序的布局练练手.感觉还是挺不错的,也封装了很多东西功能出来,与 ...

  6. 【Python】模拟登录上海西南某高校校园网 (jaccount)

    好久没写东西了,最近学习了一下模拟登录,以校园网为例,作一记录. 去年的时候写过一篇模拟登录的文章,用的是登录后的cookies,这种操作比较傻瓜,也不智能,不够自动化,本质还是手动登录. 这次我尝试 ...

  7. Python字典列表字段重组形成新的字典

    最近遇到这样一个需求,需要将字典列表中的字段进行重组,形成一个新的字典.举个例子吧: l1 = [{"x": 22, "y": 22, "demand ...

  8. 牛客多校第十场 F Popping Balloons 线段树维护稀疏矩阵

    题意: 给定一个稀疏矩阵,里面有若干个气球,让你横着开三枪,竖着开三枪,问最多能打爆多少气球,要求相同方向,相邻两枪必须间隔r. 题解: 横向记录每列有多少个气球,分别在哪行上. 然后把这个数据改造成 ...

  9. 全局唯一标识符(GUID,Globally Unique Identifier)

    全局唯一标识符(GUID,Globally Unique Identifier)是一种由算法生成的二进制长度为128位的数字标识符.GUID主要用于在拥有多个节点.多台计算机的网络或系统中.在理想情况 ...

  10. Python实现二叉堆

    Python实现二叉堆 二叉堆是一种特殊的堆,二叉堆是完全二元树(二叉树)或者是近似完全二元树(二叉树).二叉堆有两种:最大堆和最小堆.最大堆:父结点的键值总是大于或等于任何一个子节点的键值:最小堆: ...