AQS是什么:

java.util.concurrnt包中lock包下有一个抽象类:AbstractQueuedSynchronizer简称为AQS。

为实现阻塞锁和相关同步器提供一个框架,他是依赖于先进先出的等待队列。

依靠单个原子int值来表示状态,该状态用于表示锁是获取中还是释放。通过给定的方法改变状态的值。

定义了内部类ConditionObject

拥有两种线程模式

独占模式

共享模式

在LOCK包中的相关锁(常用的有ReentrantLock、 ReadWriteLock)都是基于AQS来构建。

一般我们叫AQS为同步器。

同步状态:

STATE是共享资源,描述当前有多少线程获取了锁,对于互斥锁来说state<=1。

使用volatile修饰的int类型实现线程可见性。

java 多线程中数组安全用什么 java多线程aqs_等待队列

通过使用getState()、setState(int) 和/或 compareAndSetState(int, int) 方法可以检查和/或修改同步状态。

修改state状态值时使用CAS算法来实现(乐观锁)。

java 多线程中数组安全用什么 java多线程aqs_java线程锁标记_02

AQS支持两种资源访问方式,独占模式(Exclusive)和共享模式(Share)。

AQS的子类只支持其中一种模式,但也有例外,如ReadWriteLock支持两种模式。只支持独占模式或者只支持共享模式的子类不必定义支持未使用模式的方法。

为了将此类用作同步器的基础,子类需要适当地重新定义以下方法:

tryAcquire(int)。在独占模式下获取对象状态。

tryRelease(int)。设置状态来反映独占模式下的一个释放。

tryAcquireShared(int)。在共享模式下获取对象状态。

tryReleaseShared(int)。设置状态来反映共享模式下的一个释放。

isHeldExclusively()。如果对于当前线程,同步是以独占方式进行的,则返回 true。

在AQS中,这几个方法都抛出UnsupportedOperationException。

AQS的子类(锁或者同步器)需要进行重写这几个方法实现共享资源STATE的获取与释放方式。

以ReentrantLock为例,ReentrantLock是独占锁,只有独占模式(Exclusive)一种资源访问方式,所以在NonfairSync和FairSync中只有tryAcquire(int)和tryRelease(int)方法。

state初始化为0,表示未锁定状态。线程thread调用reentrantLock.lock()时,会调用tryAcquire(1)独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到线程thread调用unlock()时,会调用tryRelease将state-1,这时state=0,这时其它线程才有机会获取该锁。

当然,ReentrantLock是可重入的锁,在释放锁之前,线程thread自己是可以多次调用lock()重复获取此锁的,state会累加。释放锁时要重复调用unlock()释放锁,获取多少次就要释放多么次,这样才能保证state是能回到零态的。

先进先出队列:

这个队列被称为:CLH队列(三个名字组成),是一个双向队列。

通过自旋锁和CAS保证节点插入和移除的原子性。

把队头head和队尾tail设置为了volatile,这两个节点的修改将会被其他线程看到,事实上,我们也主要是通过修改这两个节点来完成入队和出队

java 多线程中数组安全用什么 java多线程aqs_释放资源_03

队列中节点的状态:

SIGNAL: 节点的继任节点是(或者将要成为)BLOCKED状态(例如通过LockSupport.park()操作),因此一个节点一旦被释放(解锁)或者取消就需要唤醒(LockSupport.unpack())它的继任节点。

CANCELLED: 节点操作因为超时或者对应的线程被interrupt。节点不应该留在此状态,一旦达到此状态将从CHL队列中踢出。

CONDITION: 表明节点对应的线程因为不满足一个条件(Condition)而被阻塞。

PROPAGATE: 一个releaseShared 应该扩散到其他节点。设置在doReleaseShared方法(只是头结点)中为了确保扩散继续,即使其他操作已经开始执行。

0: 正常状态。

状态为非负值意味着节点不需要被唤醒。初始化为0,可以使用CAS修改它。

java 多线程中数组安全用什么 java多线程aqs_等待队列_04

acquire方法:以独占模式获取对象,忽略中断。

该方法用到了模板设计模式,由子类实现的。

通过至少调用一次 tryAcquire(int) 来实现此方法,并在成功时返回。

否则在成功之前,一直调用 tryAcquire(int) 将线程加入队列,线程可能重复被阻塞或不被阻塞。

java 多线程中数组安全用什么 java多线程aqs_java 多线程中数组安全用什么_05

过程:

acquire(int)尝试获取资源,如果成功,则acquire的任务就完成了。

如果获取失败,将线程标记为独占模式,插入等待队列。通过acquireQueued()自旋尝试获取资源。

如果前置节点是头结点,继续尝试获取资源,,如果成功则返回中断位结束acquire(int)。

否则,调用shouldParkAfterFailedAcquire(Node, Node)检测当前节点是否应该park()。

如果前置节点的状态是Node.SIGNAL,说明前置节点释放资源后会通知自己。此时当前节点就可以park(),返回true。

如果前置节点的状态是Node.CANCELLED,说明前置节点已经放弃获取资源了,此时一直往前找,直到找到最近的一个处于正常等待状态的节点,并排在它的后边。返回false,acquireQueued()自旋,回到第三步。

如果前置节点处于其他状态,利用CAS将前置节点的状态置为SIGNAL,让它释放资源后通知自己(有可能失败,如果前置节点刚刚释放资源,状态就不是SIGNAL了,这时就会失败)。返回false,acquireQueued()自旋,回到第三步。

如果shouldParkAfterFailedAcquire(Node, Node)返回true,调用parkAndCheckInterrupt()中断当前节点中的线程。acquireQueued(Node,int)返回true。

回到acquire(int),acquireQueued(Node,int)如果返回true,调用selfInterrupt(),中断当前线程。

tryAcquire(int)方法:

AQS的子类(锁或者同步器)需要进行重写这个方法实现共享资源STATE的获取。以公平的ReentrantLock为例。

java 多线程中数组安全用什么 java多线程aqs_等待队列_06

从代码中可以看出,如果共享资源state为0,说明该资源还没有被其他线程占用,此时tryAcquire(int)获取资源成功。如果state不为0,且资源的占用者就是当前线程,此时可以重复获取资源(印证了ReentrantLock是可重入的锁),tryAcquire(int)获取资源成功。

addWaiter(Node)方法:此方法用于将当前线程加入到等待队列的队尾,并返回当前线程所在的节点。

private Node addWaiter(Node mode) {
//将当前线程封装成节点,并指定资源访问模式,如独占模式(Exclusive)和共享模式(Share)
Node node = new Node(Thread.currentThread(), mode);
// 尝试快速方式入队
Node pred = tail;
if (pred != null) {
node.prev = pred;
//可以看出,设置尾节点是通过CAS进行的
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//如果快速方式入队失败,则通过enq(Node)入队
enq(node);
//返回当前线程所在的节点
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
//此时可能有其他线程插入,所以重新判断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(Node, int):

在把node插入队列末尾后,它并不立即挂起该节点中线程,因为在插入它的过程中,前面的线程可能已经执行完成。

所以它会先进行自旋操作acquireQueued(node, arg),尝试让该线程重新获取锁!当条件满足获取到了锁则可以从自旋过程中退出,否则继续。

final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//自旋尝试获取资源
for (;;) {
final Node p = node.predecessor();
//如果节点的前驱是队列的头结点,这时可以调用tryAcquire(arg)尝试获取资源,如果成功则返回中断位结束acquire(int)。
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//调用shouldParkAfterFailedAcquire(Node, Node)检测当前节点是否应该park()
//如果shouldParkAfterFailedAcquire(Node, Node)返回true,调用parkAndCheckInterrupt()中断当前节点中的线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire(Node, Node)方法:如果没获取到锁,则判断是否应该挂起,而这个判断则得通过它的前驱节点的waitStatus来确定。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* 如果前置节点的状态是Node.SIGNAL,说明前置节点释放资源后会通知自己。
* 此时当前节点就可以park(),返回true。
*/
return true;
if (ws > 0) {
/*
* 如果前置节点的状态是Node.CANCELLED,说明前置节点已经放弃获取资源了,
* 此时一直往前找,直到找到最近的一个处于正常等待状态的节点,
* 并排在它的后边,返回false。
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* 如果前置节点处于其他状态,利用CAS将前置节点的状态置为SIGNAL,
* 让它释放资源后通知自己
* (有可能失败,如果前置节点刚刚释放资源,状态就不是SIGNAL了,这时就会失败)。
* 返回false
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
parkAndCheckInterrupt()方法:如果shouldParkAfterFailedAcquire(Node, Node)返回true,调用parkAndCheckInterrupt()中断当前节点中的线程。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
release(int)方法: 以独占模式释放对象.
/**
* 以独占模式释放对象。
* 如果 tryRelease(int) 返回 true,则通过消除一个或多个线程的阻塞来实现此方法。
* 可以使用此方法来实现 Lock.unlock() 方法
*/
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}

release(int)会释放指定量的资源,如果资源被彻底释放了,即state=0,它会唤醒等待队列里的其他线程来获取资源。

release(int)的执行流程:

tryRelease(int)尝试直接去释放资源.如果将资源完全释放了,调用unparkSuccessor(Node)唤醒头结点的后继节点。返回true。

如果tryRelease(int)没有将资源完全释放,返回false。

tryRelease(int)方法:

AQS的子类(锁或者同步器)需要进行重写这个方法实现共享资源STATE的释放。以公平的ReentrantLock为例。

protected final boolean tryRelease(int releases) {
//释放量为releases的资源
int c = getState() - releases;
//如果当前线程不是独占模式的,抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//如果当前线程占有的资源已经全部被释放了,返回true
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
unparkSuccessor(Node)方法,唤醒后续的节点。
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
//如果节点状态小于0,将节点状态置为0,即正常状态
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)
s = t;
}
//唤醒节点
if (s != null)
LockSupport.unpark(s.thread);
}
acquireShared(int):以共享模式获取对象,忽略中断。
acquireShared(int)的执行流程:
tryAcquireShared(int)尝试直接去获取资源,如果成功,acquireShared(int)就结束了。
如果步骤1失败了,调用doAcquireShared(Node)将线程加入等待队列,直到获取到资源为止。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}

tryAcquireShared(int):需要进行重写这个方法实现共享资源STATE的获取。以ReentrantReadWriteLock为例。

protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
//如果独占锁(写锁)已经被其他线程获取,tryAcquireShared(int)失败
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
//如果线程不应该阻塞,且大小没有饱和,且CAS成功,获取资源成功,返回1
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
//如果线程应该阻塞,或大小饱和,或CAS失败,自旋获取资源,如果成功返回1,否则返回-1
return fullTryAcquireShared(current);
}
doAcquireShared(arg)方法:在等待队列中继续尝试获取资源。
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);
}
}

releaseShared(int)方法:以共享模式释放对象。releaseShared(int)与release(int)大体上比较像,区别在于releaseShared(int)在释放一部分资源后就可以通知其他线程获取资源,而release(int)要等资源释放完后才可以。

public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}

在AQS的设计中,在父类AQS中实现了对等待队列的默认实现,子类中几乎不用修改该部分功能。而state在子类中根据需要被赋予了不同的意义,子类通过对state的不同操作来提供不同的同步器功能,进而对封装的工具类提供不同的功能。

流程总结:

java 多线程中数组安全用什么 java多线程aqs_释放资源_07