文章目录
1、什么是AQS?
AQS(AbstractQueuedSynchronizer),是Java中并发包的核心组件之一,它提供了一种队列同步器的实现方式。我们并发包中经常使用的,例如ReentrantLock、CountDownLatch、Semaphore等都是通过AQS来构建实现的。它的内部是使用一个FIFO等待队列来管理线程的获取和释放锁的顺序,同时使用一个volatile变量来标识锁的状态。AQS我们可以通过继承来进行扩展,但是我们要记得实现tryAcquire和tryRelease等方法来控制锁的获取和释放
2、AQS中关键参数以及源码解析
通过这个图我们可以看出AQS继承了AbstractOwnableSynchronizer,AbstractOwnableSynchronizer这个类中主要的作用是设置线程为同步资源的独占访问权,以及获取独占资源访问的线程。
public abstract class AbstractOwnableSynchronizer
implements java.io.Serializable {
private static final long serialVersionUID = 3737899427754241961L;
protected AbstractOwnableSynchronizer() { }
/**
* 标识某个线程具有同步资源线程的独占访问权,我们可以通过判断当前锁是否已被占用,以及哪个线程占用了它
*/
private transient Thread exclusiveOwnerThread;
//设置线程为具有权限的独占线程
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
//获取独占资源访问的线程
protected final Thread getExclusiveOwnerThread() {
return exclusiveOwnerThread;
}
}
2.1 CLH锁
参考:AQS核心数据结构CLH,这篇文档介绍的更加详细
2.1.1 CLH锁介绍
AQS的队列是对CLH进行优化后的队列,所以我们要先了解一下什么是CLH锁。CLH锁是一个对自旋锁的一种改进,他会把线程组织成一个类似链表队列,所有请求锁的线程都会排列在链表队列中。每个CLH节点有两个属性,一个代表线程,另一个标识是否持有锁的状态变量,后继结点会自旋访问队列中的前驱节点,当前驱节点锁释放后,它才会结束自旋。
2.1.2 CLH锁优缺点
优点:
- 性能优异,因为CLH锁的状态不是一个单一变量,而是分散到每个节点单独的状态上,降低了自旋锁在竞争激烈时数据频繁同步的开销,释放时也不需要使用CAS操作,所以获取和释放锁的开销变小了
- 公平锁,先入队列的先获取到锁
- 可以扩展,例如AQS就是对CLH锁进行的扩展。
缺点:
- 如果一个节点持有锁时间过长,可以自旋会带来较大的CPU开销。
2.2 AQS中变体CLH队列
- 双向链表
- head指向头结点,tail指向尾结点
- 不是自旋获取前驱节点状态,而是阻塞获取前驱节点状态
- EXCLUSIVE表示独占方式,SHARED表示是共享方式
大致模型如下:
Node代码如下
static final class Node {
//表示是共享模式
static final Node SHARED = new Node();
//表示是独占模式
static final Node EXCLUSIVE = null;
//由于超时、中断、异常,会触发变更为此状态,进入该状态后的节点将不会再变化
static final int CANCELLED = 1;
//表示后继节点在等待当前节点唤醒。等后继节点入队列后,会将前驱节点的状态更新为SIGNAL
static final int SIGNAL = -1;
//表示节点在等待队列上,当其他线程调用了Condition的signal方法后,状态节点会从等待队列移动到同步队列中,等待获取同步锁
static final int CONDITION = -2;
//共享模式下,前驱节点不仅会唤醒后继结点,同事也可能唤醒后继的后继结点
static final int PROPAGATE = -3;
//队列中节点的状态
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
//是否是共享状态
final boolean isShared() {
return nextWaiter == SHARED;
}
//检查前一个节点是否为空,返回前驱节点
final Node predecessor() {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() {}
Node(Node nextWaiter) {
this.nextWaiter = nextWaiter;
THREAD.set(this, Thread.currentThread());
}
Node(int waitStatus) {
WAITSTATUS.set(this, waitStatus);
THREAD.set(this, Thread.currentThread());
}
//改变当前节点状态
final boolean compareAndSetWaitStatus(int expect, int update) {
return WAITSTATUS.compareAndSet(this, expect, update);
}
//改变下一个节点状态
final boolean compareAndSetNext(Node expect, Node update) {
return NEXT.compareAndSet(this, expect, update);
}
//设置前置节点
final void setPrevRelaxed(Node p) {
PREV.set(this, p);
}
private static final VarHandle NEXT;
private static final VarHandle PREV;
private static final VarHandle THREAD;
private static final VarHandle WAITSTATUS;
static {
try {
MethodHandles.Lookup l = MethodHandles.lookup();
NEXT = l.findVarHandle(Node.class, "next", Node.class);
PREV = l.findVarHandle(Node.class, "prev", Node.class);
THREAD = l.findVarHandle(Node.class, "thread", Thread.class);
WAITSTATUS = l.findVarHandle(Node.class, "waitStatus", int.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
}
2.3 AQS中参数
- state:表示共享资源,操作方法有下面代码中三种,资源共享方式总共分为两类,
独占方式EXCLUSIVE ,只有一个线程能执行 例如:ReentrantLock
共享方式 SHARED 多个线程同时执行例如:CountDownLatch、Semaphore
具体以什么样的逻辑来争用共享资源state,可以自定义同步器来实现不同的方式,具体怎么获取和释放由实现的自定义同步器内部自己维护。对于获取资源失败入队、唤醒出队等,AQS已经在自己内部实现完成了。
private transient volatile Node head;
private transient volatile Node tail;
private volatile int state;
protected final int getState() {
return state;
}
protected final void setState(int newState) {
state = newState;
}
protected final boolean compareAndSetState(int expect, int update) {
return STATE.compareAndSet(this, expect, update);
}
2.4 自定义同步器
由对于共享资源的方式不同来区分
- 独占方式需要实现tryAcquire()、tryRelease()。例如:ReentrantLock如果通过lock方法获取锁成功,它内部的实现同步器Sync就会调用acquire()方法,然后再调用自己实现的tryAcquire(),对state共享资源进行+1操作。后续如果线程在同步内部同步器Sync调用tryAcquire()方法时就会失败,直到获取锁的线程unlock(),把state的值更改为0,其他线程才有机会获取锁。
- 共享方式需要实现tryAcquireShared()、tryReleaseShared(),例如CountDownLatch,如果初始要N个线程来执行任务,那么state值初始值就为N,N个线程并行执行,每个线程执行完后就会countDown()一次,然后对state进行CAS操作,使值减1,所有线程执行完后state就会为0。
//尝试以独占模式获取,如果允许获取,就返回true
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
//尝试以独占模式释放,如果允许释放,就返回true
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
/**
<0共享模式获取,如果返回负数则失败
=0则证明获取成功,且后续获取失败,没有剩余资源了
>0正数则获取成功,且有剩余资源,后续也能获取成功
*/
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
//释放,如果释放允许唤醒后续等待节点则返回true,否则返回false
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
2.4.1 独占模式执行acquire()解析
acquire方法作用:
- 获取资源
- 如果获取不到资源则添加到队列中
- 然后循环判断前驱节点的状态来决定当前节点是否进入阻塞状态
- 如果当前线程是否应该中断状态,如果是则中断当前线程
2.4.1.1 acquire源码解析:
//自己实现的同步器调用acquire()方法,然后再调用tryAcquire()方法,执行自己内部实现方法的逻辑
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
//添加尾结点后则一直阻塞在队列中循环获取,直到获取到资源才返回
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//获取到资源后,才把当前线程中断
selfInterrupt();
}
//钩子方法,执行自定义同步器中内部tryAcquire()方法实现的逻辑
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
//如果获取失败,则添加节点到队列中,把当前节点更改为尾结点tail,然后前驱节点指向oldTail
private Node addWaiter(Node mode) {
Node node = new Node(mode);
//循环插入尾结点,成功后才返回
for (;;) {
Node oldTail = tail;
if (oldTail != null) {
node.setPrevRelaxed(oldTail);
if (compareAndSetTail(oldTail, node)) {
oldTail.next = node;
return node;
}
} else {
initializeSyncQueue();
}
}
}
2.4.1.2 acquireQueued源码解析
final boolean acquireQueued(final Node node, int arg) {
boolean interrupted = false;
try {
for (;;) {
//拿到当前节点的前一个节点
final Node p = node.predecessor();
/*
如果当前节点的前一个节点为头结点的话,则证明当前节点可以再次尝试获取锁,来对共享资源进行访问
*/
if (p == head && tryAcquire(arg)) {
//如果获取资源成功,则把当前节点设置为头节点,并且把前驱节点设置为null
setHead(node);
p.next = null; // help GC
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node))
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
//如果发生异常或者超时,则那节点状态改为CANCELLED,表示状态不能被更改了
cancelAcquire(node);
if (interrupted)
//然后自我中断
selfInterrupt();
throw t;
}
}
//如果前置节点状态为SIGNAL,则当前节点通过park进入waiting状态
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
//然后判断当前线程是否是中断状态
return Thread.interrupted();
}
2.4.1.3 shouldParkAfterFailedAcquire源码解析
作用
-
就是通过判断前驱节点的waitStatus状态来决定自己是否通过park方法进入waiting状态以及是否阻塞interrupted的设置,
如果前驱节点状态为SIGNAL,则自己进入waiting状态,等待调用LockSupport.unpark来唤醒。 -
如果waitStatus>0,然后一直往前遍历前驱节点,直到找到合适的前驱节点
//判断当前节点的前置节点的waitStatus状态
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
//如果前置节点状态为SIGNAL,则当前节点可以安全的去休息了,进入waiting状态
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
//如果大于0,则一直循环,把当前节点前置节点指向前置节点的前置节点,直到找到合适节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//如果都不是则把前置节点状态改为SIGNAL
pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
}
return false;
}
2.4.1.4 cancelAcquire源码解析
如果循环判断当前节点是否应该阻塞时超时或者发生异常则进入该方法,具体作用就是取消某个节点在队列中的等待状态,唤醒某个节点
作用:
- 把当前节点状态设置为CANCELLED
- 然后找到合适的前驱节点和合适的后继节点。
- 如果当前节点是tail节点则设置找到的前驱节点设置为tail节点。
- 如果前驱节点不是头结点且状态为SIGNAL或者修改为SIGNAL成功,且后继结点是一个有效节点,那么就把前驱节点和后继结点连接起来
- 如果4不满足则唤醒当前节点的后继节点
private void cancelAcquire(Node node) {
if (node == null)
return;
//线程设置为null
node.thread = null;
Node pred = node.prev;
/*
然后循环遍历判断当前节点的前驱节点,如果watiStatus > 0,则证明这个节点已经被取消,则继续往前遍历,直到找到合适的节点。
*/
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
//然后记录这个合适节点的后继结点
Node predNext = pred.next;
//然后设置node节点的waitStatus为CANCELLED
node.waitStatus = Node.CANCELLED;
//判断当前节点是否为tail节点,如果是则把这个有效节点设置为tail节点且后继节点设置为null
if (node == tail && compareAndSetTail(node, pred)) {
pred.compareAndSetNext(predNext, null);
} else {
int ws;
//如果当前节点node不是tail节点,然后这个有效节点prev不是head节点L
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
//且前驱节点waitStatus是SIGNA或者ws<=0,不是则更改为SIGNAL,且thread不为null
(ws <= 0 && pred.compareAndSetWaitStatus(ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
//后继节点不为null,且状态waitStatus <= 0
if (next != null && next.waitStatus <= 0)
//设置置这个有效节点的后继节点指针指向当前节点的后继节点
pred.compareAndSetNext(predNext, next);
} else {
//目录2.4.2.2有对这个方法的解释
unparkSuccessor(node);
}
//自己指针指向自己,便于垃圾回收
node.next = node; // help GC
}
}
2.4.1.5 acquire()整体流程图
2.4.2 独占模式执行release()解析
release方法作用:
- 释放资源
2.4.2.1 release()源码解析
//如果释放成功,则返回true,然后需要唤醒后继节点
public final boolean release(int arg) {
if (tryRelease(arg)) {
//获取头结点
Node h = head;
//头结点不为空且waitStatus状态不是默认状态
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
//钩子函数,执行自定义同步器内部tryRelease()方法实现的逻辑
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
2.4.2.2 unparkSuccessor源码解析
作用:唤醒某个节点后继节点中第一个有效节点
- 如果后继节点为null或者状态已取消,则从tail开始往前遍历,找到第一个有效节点
- 如果不为null或者状态不是已取消,则唤醒后继节点
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
//如果状态小于0(表示节点正在等待或已经等待过),则把waitStatus修改为0
if (ws < 0)
node.compareAndSetWaitStatus(ws, 0);
//获取后继节点
Node s = node.next;
//后继节点如果为null 或者已经被取消,则设置s为null
if (s == null || s.waitStatus > 0) {
s = null;
//如果后继节点为null,则从tail尾部节点开始遍历,知道找到第一个waitStatus<0的不为空
//且不等于当前节点的节点
for (Node p = tail; p != node && p != null; p = p.prev)
if (p.waitStatus <= 0)
s = p;
}
//后继节点不为null,则unpark唤醒后继结点线程,如果该线程通过LockSupport.park()方法阻塞过
//当时如果没有被阻塞过将不会有任何效果
if (s != null)
LockSupport.unpark(s.thread);
}
//unpark方法是一个native方法
public static void unpark(Thread thread) {
if (thread != null)
U.unpark(thread);
}
2.4.3 共享模式执行acquireShared()解析
作用:获取到共享资源,则返回true。获取失败则进入等待队列
2.4.3.1 acquireShared()源码解析
Semaphore、ReentrantReadWriteLock是调用acquireShared()方法这个逻辑,CountDownLatch则没有调用acquireShared()这个方法。
public final void acquireShared(int arg) {
//如果调用tryAcquireShared()方法返回>=0,则证明获取到了共享资源,小于0则获取不到
if (tryAcquireShared(arg) < 0)
//然后调用doAcquireShared()方法
doAcquireShared(arg);
}
//钩子方法,执行自定义同步器中内部tryAcquireShared()方法实现的逻辑
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
2.4.3.2 doAcquireShared()源码解析
作用就是根据前驱节点来决定当前节点做什么
- 如果是前驱节点是head节点,则再次尝试获取共享资源state,如果获取成功,则把当前节点设置为头结点,如果资源有剩余的话会尝试唤醒后继结点
- 如果前驱节点不是head,则根据前驱节点的状态来决定当前节点线程是否进入waiting状态和是否中断
- 如果上面流程发生异常,则把当前节点状态waitStatus设置为CANNELLED,然后找到有效的前驱节点和后继结点,然后让两者连接
- 最后根据interrupted来决定是否中断
private void doAcquireShared(int arg) {
//添加到队列中
final Node node = addWaiter(Node.SHARED);
boolean interrupted = false;
try {
for (;;) {
//获取到前驱节点
final Node p = node.predecessor();
//如果前驱节点为头结点,则再次尝试获取共享资源state
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
//>=0则证明获取成功,则设置当前节点为头结点,并且唤醒等待的后继节点
setHeadAndPropagate(node, r);
p.next = null; // help GC
return;
}
}
//如果获取失败,往前找可用节点,然后则判断是否阻塞当前线程,这个方法代码解析在2.4.1.3
if (shouldParkAfterFailedAcquire(p, node))
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
//如果获取资源是发生异常,则把当前节点设置CANCELLED,这个方法的源码解析在2.4.1.4
cancelAcquire(node);
throw t;
} finally {
//判断一下是否需要中断
if (interrupted)
selfInterrupt();
}
}
//如果前置节点状态为SIGNAL,则当前节点通过park进入waiting状态
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
//然后判断当前线程是否是中断状态
return Thread.interrupted();
}
2.4.3.3 setHeadAndPropagate()源码解析
- 设置节点为头节点
- 如果有多余的资源则唤醒后继等待节点
//设置节点为头结点
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
//>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();
}
}
2.4.3.4 acquireShared()整体流程图
从整体上来看,和独占模式的acquire()方法流程是很相似的,只不过共享模式下共享资源获取成功后,需要尝试唤醒后继等待节点,因为这个节点资源不一定自己用的完嘛。
2.4.4 共享模式执行releaseShared()解析
Semaphore、ReentrantReadWriteLock、CountDownLatch都是调用releaseShared()方法这个逻辑,ReentrantReadWriteLock、CountDownLatch、Semaphore默认每次只能释放一个单位资源,但是Semaphore也可以指定数量资源的释放。
2.4.4.1 acquireShared()源码解析
释放一定arg传入参数资源,如果state修改成功,则唤醒后继准备节点,失败则不执行
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
//钩子方法,执行自定义同步器中内部tryReleaseShared()方法实现的逻辑
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
2.4.4.2 doReleaseShared()源码解析
主要作用就是唤醒后继结点中第一个有效节点
private void doReleaseShared() {
//死循环
for (;;) {
Node h = head;
//如果头结点不为空且不为尾节点
if (h != null && h != tail) {
//获取状态
int ws = h.waitStatus;
//如果状态为SIGNAL
if (ws == Node.SIGNAL) {
//如果此时状态被更改了,则跳过这次循环
if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
continue;
//如果不是则唤醒后继节点中第一个有效节点
unparkSuccessor(h);
}
//状态为0,修改状态为PROPAGATE成功,跳过这次循环
else if (ws == 0 &&
!h.compareAndSetWaitStatus(0, Node.PROPAGATE))
continue;
}
if (h == head)
break;
}
}
2.5 自定义同步器实战
模仿ReentrantLock我们实现一个不可重入的MyLock
package com.code.thread.aqs;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public class MyLock implements Lock,java.io.Serializable {
private static final long serialVersionUID = -3000897897090400540L;
private final Sync sync;
public MyLock(){
sync = new Sync();
}
/**
* 自定义同步器,要继承AQS(AbstractQueuedSynchronizer)
*/
static class Sync extends AbstractQueuedSynchronizer{
private static final long serialVersionUID = -3000897897900466540L;
@Override
protected boolean tryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取具有共享资源的独占线程
Thread exclusiveOwnerThread = getExclusiveOwnerThread();
//如果线程相同,则返回false,因为不可重入
if (exclusiveOwnerThread == current){
return false;
}
//获取共享资源的值
int state = getState();
if (state == 0){
//CAS操作是否成功,期望值为0,修改为1
if(compareAndSetState(0,1)){
//设置独占线程为当前线程
setExclusiveOwnerThread(current);
return true;
};
}
return false;
}
@Override
protected boolean tryRelease(int releases) {
//获取当前线程
final Thread current = Thread.currentThread();
Thread exclusiveOwnerThread = getExclusiveOwnerThread();
if (current != exclusiveOwnerThread){
throw new IllegalMonitorStateException();
}
//获取共享资源状态
int state = getState();
boolean result = false;
if (state == 0){
throw new IllegalMonitorStateException();
}
//修改state值
if (compareAndSetState(1,0)){
setExclusiveOwnerThread(null);
result = true;
}
return result;
}
final boolean isLocked() {
return getState() == 1;
}
Condition newCondition() {
return new ConditionObject();
}
}
@Override
public void lock() {
sync.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
sync.release(1);
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
public boolean isLocked() {
return sync.isLocked();
}
}
测试demo:
下面是测试没有使用MyLock的输出区别:
- 没有使用MyLock
public class MyLockTest {
private static int NUM = 10;
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(() -> {
getNumTwo();
});
thread.start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static MyLock myLock = new MyLock();
public static void getNum(){
myLock.lock();
try {
//模仿并发
Thread.sleep( 100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("numOne的值为:"+ --NUM);
myLock.unlock();
}
public static void getNumTwo(){
try {
//模仿并发
Thread.sleep( 100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("numTwo的值为:"+--NUM);
}
}
输出值为:可以从输出结果上明显感受出问题,并没有按照我们想要的顺序输出,而且有重复的值
- 使用MyLock,输出结果如下,可以看出不管是顺序上还是结果上都是符合我们预期的,看来MyLock是生效了。这只是一个简单的demo分享。
3 小结
从我刚开始看源码到最后文章输出,当看完后,再去看ReentrantLock、CountDownLatch等源码时,发现思路和他们为什么这么写有了更加清楚的认知,希望这篇文章能够帮助大家对AQS有一定的了解,以及看ReentrantLock、CountDownLatch等源码实现时有一定帮助。