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. Jmeter自定义Java请求开发

    一.本次实验目的 IDEA新建maven项目,使用java开发自定义jmeter的请求. 本次开发使用的代码,会百度云分享给大家. 二.本次实验环境 Idea 2017.02 Jmeter 5.1.1 ...

  2. vue+element项目中使用el-dialog弹出Tree控件报错问题

    1. 按正常的点击按钮,显示dialog弹出的Tree控件,然后把该条数据下的已经选中的checkbox , 用setCheckedNodes或者setCheckedKeys方法选择上 , 报下面这个 ...

  3. HBase的优化

    HBase的优化 高可用 在 HBase 中 Hmaster 负责监控 RegionServer 的生命周期,均衡 RegionServer 的负载,如果 Hmaster 挂掉了,那么整个 HBase ...

  4. hmm隐马尔可夫真的那么难吗?

    hmm隐马尔可夫真的那么难吗? 首先上代码 这里是github上的关于hmm的:链接 概率计算问题:前向-后向算法 学习问题:Baum-Welch算法(状态未知) 预测问题:Viterbi算法 htt ...

  5. 【Gym - 101002F】Mountain Scenes(dp)

    Mountain Scenes Descriptions 给你一个长度为n的丝带,一个宽w一个高h 的 格子,用丝带去填充格子,这填充后只需要满足至少有一列的丝带长度与其他格子不同即可.丝带可以不全部 ...

  6. Winform 连接Web Service 记录

    一般自己控制的项目都会使用webApi,比较少使用WS,感觉要配置一堆东西很繁琐. 场景:多个系统间数据交互. 角色:我们属于下游系统,要把一部分数据格式化后上传到SAP中. SAP提供了一个WS,使 ...

  7. join,列表和字典用for循环的删除,集合,深浅拷贝

    1.join() 将列表转换成字符串,并且每个字符之间用另一个字符连接起来,join后面必须是可迭代的对象(字符串,列表,元组,字典,集合),数字不能迭代 例如: s = ['a','b','c'] ...

  8. Tomcat(Linux)

    百度云:链接:http://pan.baidu.com/s/1o7MNTJc    密码:cfof 官方下载网址:http://archive.apache.org/dist/tomcat/tomca ...

  9. linux初学者-系统服务的控制

      linux系统中系统服务的控制是比较重要的一部分,这也直接影响到计算机的使用,以下将会介绍一些系统服务的控制. 1.系统服务命令 系统的初始化程序是系统开始的第一个进程,pid为1.可以通过以下命 ...

  10. eval 与 exec, compile区别

    exec 不是表达式: python 2. x, 中的一个语句和 python 3. x. 中的一个函数它编译并立即计算一个字符串中包含的语句或者语句集. 例如: exec('print(5)') # ...