AQS是个啥?

AQS(AbstractQueuedSynchronizer)是Java并发用来构建锁和其他同步组件的基础框架。许多同步类实现都依赖于它,如常用的ReentrantLock/ReentrantReadWriterLock/CountDownLatch等
 
AQS提供了独占(Exclusive)以及共享(Share)两种资源共享方式:
acquire(acquireShare)/release(releaseShare)。 
acquire:获取资源,如果当前资源满足条件,则直接返回,否则挂起当前线程,将该线程加入到队列排队。
release:释放资源,唤醒挂起线程
 
 

AQS队列

AQS队列示意图

AQS队列中的主要属性

//  等待队列头部
private transient volatile Node head; // 等待队列尾部
private transient volatile Node tail; // 锁的状态(加锁成功则为1,解锁为0,重入再+1)
private volatile int state; // 当前持有锁的线程,注意这个属性是从AbstractOwnableSynchronizer继承而来
private transient Thread exclusiveOwnerThread;

Node类中的主要属性

static final class Node {
// 标记表示节点正在共享模式中等待
static final Node SHARED = new Node();
// 标记表示节点正在独占模式下等待
static final Node EXCLUSIVE = null; // 节点的等待状态 还有一个初始化状态0 不属于以下四种状态
// 表示Node所代表的当前线程已经取消了排队,即放弃获取锁
static final int CANCELLED = 1;
// 当一个节点的waitStatus被置为SIGNAL,就说明它的下一个节点(即它的后继节点)已经被挂起了(或者马上就要被挂起了),
// 只要前继结点释放锁,就会通知标识为SIGNAL状态的后继结点的线程执行
static final int SIGNAL = -1;
// 节点在等待队列中
// 当其他线程对Condition调用了signal()后,该节点将会从等待队列中转移到同步队列中,加入到同步状态的获取中
static final int CONDITION = -2;
// 表示下一次共享式同步状态获取,将会无条件地传播下去
static final int PROPAGATE = -3; // 节点等待状态,该字段初始化为0,
volatile int waitStatus; // 当前节点的前置节点
volatile Node prev; // 当前节点的后置节点
volatile Node next; // 在此节点上排队的线程信息
volatile Thread thread;
}

ReentrantLock实现

在引入ReentrantLock实现前,我先来科普一下 util.concurrent包的作者Doug Lea,相比较其他而言,并发包的源码阅读难度较大。脸上永远挂着谦逊腼腆笑容的Doug Lea先生使用了大量相对复杂的逻辑判断,比如一个判断条件中执行多个或且方法,让你很难跟上他的节奏,很难揣摩他的设计思想。小声逼逼,还不是我太菜了,留下来没有技术的泪水。

继承关系图

ReentrantLock是Lock接口的一个实现类,是一种可重入的独占锁。
ReentrantLock内部通过内部类实现了AQS框架(AbstractQueuedSynchronizer)的API来实现独占锁的功能。

主要属性

private final Sync sync;

// 公平锁内部是FairSync,非公平锁内部是NonfairSync。
// 两者都通过继承 Sync间接继承自AbstractQueuedSynchronizer这个抽象类
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L; // 加锁
abstract void lock(); // 尝试获取锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
} // 尝试释放锁
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
}

构造方法

//默认创建一个非公平锁
public ReentrantLock() {
sync = new NonfairSync();
} //传入true创建公平锁,false非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

ReentrantLock公平锁

我们以公平锁为例对其中重要方法源码分析

// 继承了 Sync,从而间接继承了 AbstractQueuedSynchronizer这个抽象类
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L; // 上锁
final void lock() {
//调用 AQS 中 acquire方法
acquire(1);
} protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
// CAS操作设置 state
// 设置当前线程为拥有锁的线程
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}

acquire方法源码分析

public final void acquire(int arg) {
// tryAcquire(arg)尝试加锁,如果加锁失败则会调用acquireQueued方法加入队列去排队,如果加锁成功则不会调用
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquire方法干了这么几件事情
1、tryAcquire() 尝试获取资源,如果成功则直接返回;
2、addWaiter() 将该线程加入等待队列, 更新AQS队列链信息
3、acquireQueued() 使线程在等待队列中获取资源,直到获取资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
4、selfInterrupt() 自我中断,如果线程在等待过程中被中断过,它是不响应的。只是获取资源后再将中断补上。
 

tryAcquire方法

protected final boolean tryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取lock对象的上锁状态,如果锁是自由状态则=0,如果被上锁则为1,大于1表示重入
int c = getState(); // c=0 代表没人占用锁,当前线程可以直接获取锁资源执行
if (c == 0) {
// 下面介绍hasQueuedPredecessors()方法,判断自己是否需要排队
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
// CAS操作设置 state
// 设置当前线程为拥有锁的线程
setExclusiveOwnerThread(current);
return true;
}
} // 非重入锁直接返回false,加锁失败
else if (current == getExclusiveOwnerThread()) {
// 若为重入锁, state 加1 (acquires)
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

hasQueuedPredecessors方法

public final boolean hasQueuedPredecessors() {
// 获取队列头、尾节点信息
Node t = tail;
Node h = head;
Node s;
// h != t 有几种情况
// 1、队列尚未初始化完成,第一个线程获取锁资源,
// 此时h和t都是null, h != t返回fasle初始化队列
// 2、队列已经被初始化了,其他的线程尝试获取资源,
// 此时头尾节点不相同,h!=t返回true,
// 继续判断s.thread != Thread.currentThread() 当前来参与竞争锁的线程和第一个排队的线程是同一个线程,则需要排队。
// 3、队列已经被初始化了,但是由于锁释放的原因导致队列里面只有一个数据
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}

addWaiter方法

private Node addWaiter(Node mode) {
// AQS队列中的元素类型为Node,需要把当前线程封装成为一个Node对象
Node node = new Node(Thread.currentThread(), mode); // tail为队尾,赋值给pred
Node pred = tai
// 判断pred是否为空,其实就是判断队尾是否有节点,其实只要队列被初始化了队尾肯定不为空,
if (pred != null) {
// 拼装node队列链的过程
// 直接把当前线程封装的node的上一个节点设置成为pred即原来的队尾
node.prev = pred;
if (compareAndSetTail(pred, node)) {
// pred的下一个节点设置为当node
pred.next = node;
return node;
}
} // 拼接aqs队列链
enq(node);
return node;
} private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}

acquireQueued方法

final boolean acquireQueued(final Node node, int arg) {
// 标记是否成功拿到资源
boolean failed = true;
try {
// 标记等待过程中是否被中断过
boolean interrupted = false;
// 自旋
for (;;) {
final Node p = node.predecessor();
// 判断自己是否为队列中的第二个节点
// 成为队列中第二个节点才有资格获取资源
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
// 返回等待过程中是否被中断过
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

公平锁和非公平锁的主要区别

为了方便对比,在这里列举了两种锁的上锁过程源码,注意红色标识片段

// 公平锁上锁过程
final void lock() {
//调用 AQS 中 acquire方法
acquire(1);
}  
// 非公平锁上锁过程
final void lock() {
// 尝试获取锁,加锁不成功则排队。排队之前仅有的一次插队机会。
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}

总结

1、如果第一个线程尝试获取资源时,此时和AQS队列无关,线程直接持有锁。并且不会初始化队列,如果接下来的线程都是交替执行,那么和AQS队列永远无关,均为线程直接持有锁。
2、在线程发生资源竞争的情况下,才会初始化AQS队列,AQS队列的头部永远是一个虚拟的Thread为NULL的node。
3、未能获取到资源的线程将会处于park状态,此时只有队列中第二个node等待被唤醒,尝试去获取资源。其他node并不去竞争资源,这也是AQS队列的精髓所在,减少了CPU的占用。
4、公平锁的上锁是必须判断自己是不是需要排队;而非公平锁是直接进行CAS修改计数器看能不能加锁成功;如果加锁不成功则乖乖排队(调用acquire);所以不管公平还是不公平;只要进到了AQS队列当中那么他就会排队;一朝排队;永远排队!

盘一盘 AQS和ReentrantLock的更多相关文章

  1. U盘启动盘的制作--用U盘硬装Windows系统、或是重装Windows系统

    借助IT天空的优启通U盘启动盘的制作--用U盘装Windows系统.或是重装Windows系统之U盘启动盘的制作 1.==================================== 2.== ...

  2. 制作centos的U盘启动盘

    制作centos的U盘启动盘比ubuntu麻烦一些,因为可能涉及到fat32文件格式不支持大于4G的文件存储的问题,而最新版本的centos就是大于4G的,所以就需要对U盘进行分区. 一个做主引导,一 ...

  3. U盘启动盘 安装双系统 详细教程

    U盘启动盘 安装win7+linux双系统 最近在看鸟哥的linux 私房菜 ,看到多重系统那部分,自然的安装多重系统的激情由此而燃.在网上看了很多资料,感觉都不全.经过艰辛的摸索,终于被我发现了一个 ...

  4. windows下制作linux U盘启动盘或者安装优盘(转)

    windows下制作linux U盘启动盘或者安装优盘(转) Linux发行版排行榜:http://iso.linuxquestions.org/ [方案一]:UltraISO(不推荐,在Window ...

  5. 制作U盘启动盘及安装操作系统的方法

    U盘启动盘制作方法: 1.从网上下载最新的老毛桃U盘启动制作工具主程序并安装 2.插入U盘(制作启动盘前先保存好你的资料到其它地方,以防丢失不可找回) 3.插入正确的U盘后程序会自动检测到U盘,启动模 ...

  6. U深度利用iso文件制作U盘启动盘

    利用U盘装win10系统: 工具:U深度装机版   文件:win10.iso 步骤1:下载U深度装机版安装 步骤2:打开U深度,制作U盘启动盘,注意选择iso模式,如下图所示 接下来下一步即可,工具会 ...

  7. 用UltraISO制作支持windows 7的U盘启动盘

    用UltraISO制作U盘启动盘,有人写过,我也看过,不过依照网上的那些文章,成功的并不多,经过几次试验,在不同的主板环境下成功概率高的方法应该如下:   1. UltraISO建议9.3以上 2. ...

  8. UltraISO制作U盘启动盘安装Win7/10系统攻略

    UltraISO制作U盘启动盘安装Win7/9/10系统攻略 U盘安装好处就是不用使用笨拙的光盘,光盘还容易出现问题,无法读取的问题.U盘体积小,携带方便,随时都可以制作系统启动盘. U盘建议选择8G ...

  9. Windows-002-U盘启动盘制作

    通常我们安装系统时,均采用光盘的形式安装,只是这种方法需要随时随地的带着光盘,还不容易保存.携带光盘.这时,一个 U盘启动盘 就是您的首选了,此种方式的好处多多,比如:忘记开机密码.系统备份.安装系统 ...

  10. 一键制作u盘启动盘教程

    第一步:制作完成u深度u盘启动盘   第二步:下载Ghost Win7系统镜像文件包,存入u盘启动盘   第三步:电脑模式更改成ahci模式,不然安装完成win7系统会出现蓝屏现象 正式安装步骤: u ...

随机推荐

  1. bzoj3125: CITY 题解

    3125: CITY Time Limit: 10 Sec  Memory Limit: 128 MBSubmit: 486  Solved: 213[Submit][Status][Discuss] ...

  2. css基础4

    今天是2019年6月21日,周五了.在这里写上一篇随笔,主要内容是css基础中的一些细节部分,话不多说,直接上! 一.背景渐变: background-image 线性渐变:linear-gradie ...

  3. Excel催化剂开源第19波-一些虽简单但不知道时还是很难受的知识点

    通常许多的知识都是在知与不知之间,不一定非要很深奥,特别是Excel这样的应用工具层面,明明已经摆在那里,你不知道时,永远地不知道,知道了,简单学习下就已经实现出最终的功能效果. 在程序猿世界里,也是 ...

  4. java:选择排序法对数组排序

    最近想练一练Java的算法,然后碰到LeetCode上一道从排序数组删除重复项的小题,刚开始没看到是从排序数组中,就乱写,其实要是排序树组,就比乱序的感觉上好写多了.然后就想回顾下冒泡法对数组排序,凭 ...

  5. python调用WebService遇到的问题'Document' object has no attribute 'set'

    代码: from suds import WebFault from suds.client import Client url = 'http://******/bns/PtDataSvc.asmx ...

  6. Spring Boot如何设计防篡改、防重放攻击接口

    Spring Boot 防篡改.防重放攻击 本示例要内容 请求参数防止篡改攻击 基于timestamp方案,防止重放攻击 使用swagger接口文档自动生成 API接口设计 API接口由于需要供第三方 ...

  7. istio使用教程

    kubernetes各版本离线安装包 安装 安装k8s 强势插播广告 三步安装,不多说 安装helm, 推荐生产环境用helm安装,可以调参 release地址 如我使用的2.9.1版本 yum in ...

  8. 最火的分布式 HTAP 数据库 TiDB - 入门实践教程

    偶然在某篇博客看到了 TiDB,一个融合 OLTP 和 OLAP 的分布式开源数据库, GitHub 上 Star 很多,然后 watch 了,发现 commit 和 pull request 一直都 ...

  9. [Chat]实战:仿网易云课堂微信小程序开发核心技术剖析和经验分享

    本Chat以一个我参与开发并已上线运营近2年——类似网易云课堂的微信小程序项目,来进行微信小程序高级开发的学习. 本场Chat围绕项目开发核心技术分析,帮助你快速掌握在线视频.音频类小程序开发所需要的 ...

  10. 夯实Java基础(五)——==与equals()

    1.前言 我们在学习Java的时候,看到==.equals()就认为比较简单,随便看了一眼就过了,其实你并没有深入去了解二者的区别.这个问题在面试的时候出现的频率比较高,而且据统计有85%的人理直气壮 ...