前言
什么是AQS?
AQS的实现原理是什么样?
AQS源码分析
ReentrantLock加锁流程图
想要lock加锁全流程图,请看到最后,在最下方。
传送门:AQS应用之ReentrantLock解锁分析下(源码级别、流程图)
一、 什么是AQS?
Java并发编程核心在于java.util.concurrent包,而JUC当中的大多数同步器实现都是围绕着共同的基础行为,比如等待队列、条件队列、独占获取、共享获取等,而这个行为的抽象就是基于AbstractQueuedSynchronizer简称AQS,AQS定义了一套多线程访问共享资源的同步器框架,是一个依赖状态(state)的同步器。
AQS特性
阻塞等待队列,共享/独占,公平/非公平,可重入,允许中断
1、基于AQS实现一个自定义同步器一般分为2步
1、一般通过定义内部类Sync继承AQS
2、将同步器所有调用都映射到Sync对应的方法
自定义同步器实现主要实现以下几种方法:
isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
按照需要的实现即可,不是全部都要实现。
核心功能在AQS在顶层已经实现好了,直接调用即可,如具体线程等待队列的维护(如获取资源失败入队/唤醒出队等)。
2、AQS的二种队列
1、同步等待队列
也被称为CLH队列,是一种基于双向链表数据结构的队列,是FIFO先进先出的等待队列,是一种阻塞队列(如ReentrantLock就采用这种队列)。主要用于维护获取锁失败时入队的线程。
先看一下这个流程图,有个印象,后面对照源码集合看,便于理解。

2、条件等待队列
是一种多线程协调的通信的工具类,使得某些线程一起等待某个条件,等待条件具备时,这些线程才会被唤醒。(工具类如CountDownLatch倒计时锁就采用这种队列)。
源码的使用场景
调用await()的时候会释放锁,然后线程会加入到条件队列,调用
signal()唤醒的时候会把条件队列中的线程节点移动到同步队列中,等待再次获得锁

3 AQS的两种资源共享方式
Exclusive-独占,只有一个线程能执行,如ReentrantLock。
Share-共享,多个线程可以同时执行,如Semaphore/CountDownLatch。
4 可重入
可重入是指,在线程已持有锁的情况下,无需重新去争夺锁,节约不必要的开销。
AQS内部维护的核心属性,AQS就是基于这个状态属性实现的重入。
如果第一次进来直接抢锁,拿到了就设置锁线程是当前线程,并且状态为1.
第二次进来加锁,发现线程锁的线程对象就是当前线程,则直接获取到锁,状态码进行累加1.
/**
* 同步资源状态
*/
private volatile int state;
简单理解就是,可能A方法内部有个加锁,然后这时候去调用B方法,B方法也有不同加锁的操作的场景。
假设线程1执行A方法已经抢到锁了,这时候在A方法里面调用B方法,B方法去加锁的时候,不需要再重新去抢锁,而是看是不是重入的,重入就直接累加1,就好了。
如果没有重入锁机制
上述场景就会出现,A方法抢到锁了,去执行B方法,B方法没抢到锁,最终就死锁了。
5 公平/非公平
说到公平、非公平,就需要拿ReentrantLock来说明了。
ReentrantLock是一种基于AQS框架实现的同步器,是JDK中的一种线程并发访问的同步手段,它的功能和synchronized类似,是一把互斥锁,可以保证线程安全。他相较于synchronized具有更多的特性,比如它支持手动加锁、解锁。支持加锁的公平性。
在ReentrantLock内部定义了一个Sync的内部类,该类继承AbstractQueuedSynchronized,对该抽象类的部分方法做了实现;并且还定义了两个子类:
1、FairSync 公平锁的实现,按队列排序去获取锁;
2、NonfairSync 非公平锁的实现,不排队,谁抢到就是谁的。
这两个类都继承自Sync,也就是间接继承了AbstractQueuedSynchronized,所以这一个ReentrantLock同时具备公平与非公平特性。
为什么要搞一个公平和非公平的概念实现?
1、公平可以保证按顺序来
2、非公平适合那些不排序的情况,并发量高的时候还可以提升并发量。
这里涉及到一个设计模式
这是加锁方法都会调用的acquire方法,里面的tryAcquire方法都是由各自的子类实现。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

上面主要涉及的设计模式:模板模式-子类根据需要做具体业务实现
二、AQS、ReentrantLock源码分析
以ReentrantLock这3行代码开始拆解,,主要以讲核心AQS源码为主。
ReentrantLock lock = new ReentrantLock(true);//false为非公平锁,tru e为公平锁
lock.lock(); //加锁
lock.unlock(); //解锁
2.0、 AQS的核心内部类Node和核心属性
先大致看一下核心的属性,方便后面源码的理解。
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
/**
* Wait queue node class.
* <pre>
* +------+ prev +-----+ +-----+
* head | | <---- | | <---- | | tail
* +------+ +-----+ +-----+
* </pre>
static final class Node {
/** 标记节点为共享模式 */
static final Node SHARED = new Node();
/** 标记节点为独占模式 */
static final Node EXCLUSIVE = null;
/** 在同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消等待 */
static final int CANCELLED = 1;
/** 后继节点的线程处于等待状态,而当前的节点如果释放了同步状态或者被取消,
* 将会通知后继节点,使后继节点的线程得以运行。 */
static final int SIGNAL = -1;
/** 节点在等待队列中,节点的线程等待在Condition上,当其他线程对Condition调用了signal()方法后,
* 该节点会从等待队列中转移到同步队列中,加入到同步状态的获取中 */
static final int CONDITION = -2;
/**
* 表示下一次共享式同步状态获取将会被无条件地传播下去
*/
static final int PROPAGATE = -3;
/**
* 标记当前节点的信号量状态 (1,0,-1,-2,-3)5种状态
* 使用CAS更改状态,volatile保证线程可见性,高并发场景下,
* 即被一个线程修改后,状态会立马让其他线程可见。
*/
volatile int waitStatus;
/**
* 前驱节点,当前节点加入到同步队列中被设置
*/
volatile Node prev;
/**
* 后继节点
*/
volatile Node next;
/**
* 节点同步状态的线程
*/
volatile Thread thread;
/**
* 等待队列中的后继节点,如果当前节点是共享的,那么这个字段是一个SHARED常量,
* 也就是说节点类型(独占和共享)和等待队列中的后继节点共用同一个字段。
*/
Node nextWaiter;
/**
* 如果节点在共享模式下等待,则返回true。
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* 返回前驱节点
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
// 空节点,用于标记共享模式
Node() { // Used to establish initial head or SHARED marker
}
//用于同步队列CLH
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
//用于条件队列
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
/**
* 指向同步等待队列的头节点
*/
private transient volatile Node head;
/**
* 指向同步等待队列的尾节点
*/
private transient volatile Node tail;
/**
* 同步资源状态.
*/
private volatile int state;
}
Node节点说明:
不管是条件队列,还是CLH等待队列
* 都是基于Node类
*
* AQS当中的同步等待队列也称CLH队列,CLH队列是Craig、Landin、Hagersten三人
* 发明的一种基于双向链表数据结构的队列,是FIFO先入先出线程等待队列,Java中的
* CLH队列是原CLH队列的一个变种,线程由原自旋机制改为阻塞机制。
*
2.1、AQS的源码核心实现方法
lock.lock(); //加锁
这里以公平锁的实现来开始拆解内部实现,lock()就是去获取公平锁。
获取锁的三种情况:
1、如果锁没有被其他线程持有,则获取锁并立即返回,并将锁持有计数设置为1。
2、如果当前线程已经持有该锁,那么持有计数将增加1,该方法将立即返回。
3、如果锁被另一个线程持有,那么当前线程就会因为线程调度的目的而被禁用,并处于休眠状态,直到获得锁,此时锁持有计数被设置为1。
final void lock() {
acquire(1);
}
2.2、acquire(AQS的核心方法)
/**
* 获取独占锁
*/
public final void acquire(int arg) {
//尝试获取锁
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//独占模式
selfInterrupt();
}
acquire做了3个事情,
1、tryAcquire拿到锁了就直接结束,
2、拿不到就去排队(执行addWaiter(Node.EXCLUSIVE)和acquireQueued方法)。
3、如果中断了,则
2.3、tryAcquire方法-公平锁的实现(尝试获取锁)
/**
* tryAcquire的公平版。除非递归调用或没有等待者或是第一个,否则不要授予访问权。
*/
protected final boolean tryAcquire(int acquires) {
// 1、获取当前执行的线程对象的引用,getState()获取当前状态值。
final Thread current = Thread.currentThread();
int c = getState();
// 2、如果状态值为0,并且同步队列为空,则去CAS抢锁,抢到锁了,就设置锁的线程为当前线程,并结束。
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 3、如果不=0,并且当前线程已经持有该锁,状态码累加1,更新下状态值结束。
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 4、以上2个条件都不满足,说明没抢到锁,直接返回false,就去排队;
return false;
}
}
tryAcquire方法做了几件事情:
1、获取当前执行的线程对象的引用,getState()获取当前状态值。
2、如果状态值为0,并且同步队列为空,则去CAS抢锁,抢到锁了,就设置锁的线程为当前线程,并结束。
3、如果不=0,并且当前线程已经持有该锁,状态码累加1,更新下状态值结束。
4、以上2个条件都不满足,说明没抢到锁,直接返回false,就去排队;
2.4、addWaiter-AQS核心方法(添加新节点到队列尾部)
为当前线程和给定模式创建和排队节点。参数:mode—Node。EXCLUSIVE表示EXCLUSIVE, Node表示节点。返回:新节点
private Node addWaiter(Node mode) {
// 1、创建新节点,新节点的下一节点为null。
Node node = new Node(Thread.currentThread(), mode);
// 2、获取队队列尾部节点,尾节点不为null(说明队列已初始化)
Node pred = tail;
if (pred != null) {
node.prev = pred;
// 2.1、判断能不能cas添加到尾部节点,成功则直接返回。
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 3、执行enq方法,进行队列初始化
enq(node);
return node;
}
主要做如下几件事情:
1、创建新节点,新节点的下一节点为null。
2、获取队队列尾部节点,尾节点不为null(说明队列已初始化)
2.1、判断能不能cas添加到尾部节点,成功则直接返回。
3、执行enq方法,进行队列初始化
2.5、enq-AQS核心方法(初始化队列,和插入队列)
将节点插入队列,必要时初始化
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // 1、获取队列尾部节点,队列尾部节点为空,则创建新节点,自旋尝试CAS添加头部节点。
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
// 2、如果队列尾部节点不为空,设置新创建的节点上一节点为队列尾部节点,并且CAS自旋尝试设置为队列尾部节点
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
做了如下几件事情:
1、获取队列尾部节点,队列尾部节点为空,则创建新节点,自旋尝试CAS添加头部节点。
2、如果队列尾部节点不为空,设置新创建的节点上一节点为队列尾部节点,并且CAS自旋尝试设置为队列尾部节点
2.6、acquireQueued-AQS核心方法(再次抢锁、抢不到堵塞当前线程)
拿不到锁的情况下,才会进入这个方法,去对当前线程以及同步队列进行处理。
以独占不可中断模式获取已在队列中的线程。用于条件等待方法以及获取方法。参数:node -节点如果在等待时被中断,则返回:true
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 1、自旋
for (;;) {
// 2、获取队列原尾节点
final Node p = node.predecessor();
// 3、如果原尾节点是队列头部节点,并且可尝试获取锁成功,则直接更新队列头部节点指针
if (p == head && tryAcquire(arg)) {
setHead(node);
// 为了让他垃圾回收,不占用队列的空间内存
p.next = null; // help GC
failed = false;
// 3.1返回不中断,结束加锁方法
return interrupted;
}
// 4、判断原尾巴节点的状态,根据状态的不同,进行相应的处理,
// 如阻塞当前节点、原尾巴节点移除队列。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
做了如下几件事情
1、自旋
2、获取原队列尾巴节点
3、如果原尾节点是队列头部节点,并且可尝试获取锁成功,则直接更新队列头部节点指针
3.1、拿到锁了,返回不中断,结束自旋,并且结束加锁方法
4、判断原尾巴节点的状态,根据状态的不同,进行相应的处理,如阻塞当前节点、原尾巴节点移除队列。
判断原尾巴节点的状态是否为SIGNAL(等待),如果他处于等待状态,则调用parkAndCheckInterrupt())对当前线程进行堵塞。
2.7、shouldParkAfterFailedAcquire-堵塞线程,处理同步队列
主要是堵塞线程,处理同步队列的元素位置等。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* 若前驱结点的状态是SIGNAL,意味着当前结点可以被安全地park
*/
return true;
if (ws > 0) {
/*
* 前驱节点状态如果被取消状态,将被移除出队列
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* 当前驱节点waitStatus为 0 or PROPAGATE状态时
* 将其设置为SIGNAL状态,然后当前结点才可以可以被安全地park
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
主要做了以下几件事情:
1、判断原尾巴节点是不是等待,是的话,则直接进行堵塞。
2、如果原尾巴节点被中断,则移除队列,并且调整当前节点的队列元素位置。
3、自旋,cas的进行更新原尾巴节点的状态设置为等待。
三、公平锁加锁源码的流程图

四、源码分析图-模拟3个线程加锁、解脱流程图文形式
4.1、线程0进来加锁逻辑
1、线程0进来由于没有线程持有锁,直接加锁成功,并且初始化队列。

4.2、线程1加锁逻辑
线程1进来尝试加锁,加锁失败
开始入队,尾节点为线程1,并且节点线程为线程1,
入队成功后,进行阻塞,
这里需要注意,堵塞当前节点的的前一节点状态是-1,持有锁线程为表示代表可以唤醒

4.3、线程2加锁逻辑
线程2加锁,加锁失败,先入队,链表尾插法,插到队尾,设置节点线程为线程2
这里需要注意,和线程1插入不同的是,会把线程1的状态也设置为-1。
4.4、线程0释放锁
直接解锁,不需要CAS,直接设置为0,期间会判断下有没有重入锁,
然后唤醒队列前做的事情,
头部节点的状态设置为0,唤醒最靠前头部节点的节点,然后调用LockSupport.unpark(s.thread)真正唤醒。

4.5、线程1,加锁成功
线程1被唤醒后,进来尝试去加锁,然后加锁成功,
处理把头部节点设置为线程节点,并且把原本指向头部节点的指针去掉,持有线程也置空,
原头部节点下一节点指针也置空,最终原头部节点置空,好让垃圾回收

总结
AQS核心实现原理:
自旋/CAS/队列(双向链表FIFO)
本文深入解析了AQS框架及其在ReentrantLock中的应用,详细介绍了AQS的内部结构、工作原理以及ReentrantLock的加锁流程。通过源码分析,帮助读者理解公平锁与非公平锁的区别及其实现细节。
170万+

被折叠的 条评论
为什么被折叠?



