CAS能够原子的对一个值进行写操作,那么可以将这个值(称为status)作为竞争资源的标记位。在多个线程想要去修改共享资源时,先来读取status,看能不能获取到写status的权限。
拒绝其它线程的调用怎么设计呢?有两种业务场景:有的业务可能只是快速尝试获取一下共享资源,获取不到也没关系,会进行其它处理,有的业务线程一定要获取共享资源才能进行下一步处理,如果没有获取到,愿意等待。第一种场景直接返回共享资源当前的状态就可以了,第二种场景不适合自旋,我们设计一个队列让这些线程排队。队列头部的线程自旋访问status,其它线程挂起,避免了大量资源的消耗。当头部线程成功占用了共享资源,那么它在唤醒后续一个被挂起的线程,让它开始自旋的访问status。
AQS,一个抽象的(可被继承和复用),内部存在排队(竞争资源的线程排队)的同步器(对共享资源和线程进行同步管理)
private volatile int state;
state就是之前说的判断共享资源是否被占用的标记位,volatile保证了线程之间的可见性,简单来说就是一个线程修改了state的值,其它线程下一次能读取到最新值。
为什么不是boolean而是int?
锁有独占和共享,共享模式下可能有多个线程正在共享资源,所以state需要表示线程占用数量,因此是int值。
private transient volatile Node head;
private transient volatile Node tail;
AQS用一个队列用于对等待的线程进行管理,这个队列通过一个FIFO的双向链表来实现。
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
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() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
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;
}
Node主要存储了线程对象(thread),节点在队列里的等待状态(waitStatus),前后指针等信息。重点关注waitStatus这个属性,它是一个枚举值,AQS工作时必然伴随Node的waitStatus值的变化。
Node中的方法也很简单,prodecessor就是获取前置Node。
内容非常简单,后面我们要重点关注的则是如何利用state和FIFO的队列来管理多线程的同步状态,这些操作被封装成了方法。
一开始提供了两种使用场景:
AQS最上层应该会有这两个方法。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
truAcquire是一个被protected修饰的方法,参数是一个int值,代表对int state的增加操作,返回boolean代表是否成功获得锁。
很明显,AQS规定继承类鄙视重写tryAcquire方法,否则就直接抛出。
为什么要上层自己实现?
因为尝试获取锁这个操作中可能包含某些业务自定义的逻辑,比如是否可重入等。
若上层调用tryacquire返回true,可对共享资源进行操作。返回false,如果不想等待,可以进行自己的处理;如果上层选择等待锁,那么直接调用acquire方法,acquire方法内部封装了复杂的排队处理逻辑。
下面来看acquire方法
if条件包含两部分:
1如果为false代表已经获取到锁,不需要排队。
假如tryAcquire返回false,说明需要排队,执行2判断,2里面嵌套了addWaiter方法。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
顾名思义,方法的作用就是将当前线程封装成一个Node,然后假如等待队列,返回值即为该Node。逻辑非常简单,首先是新建一个Node对象,将其插入队尾。范式需要考虑多线程场景,假设存在多个线程正在同时调用addWaiter方法。
新建pred节点引用,指向当前的尾节点,如果尾节点不为空,那么下面将进行三步操作:
看一下enq方法
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;
}
}
}
}
这个方法里的逻辑,在最外层加上一层死循环,如果队列未初始化(tail==null),那么就尝试初始化,如果尾插节点失败,呢么就不断重试,知道插入成功为止。
一旦addWaiter成功之后,那么就不能这么不管了,AQS在各个线程中为花了当前Node的waitStatus,根据不同的状态,程序来做出不同的操作。通过调用acquireQueued方法,开始对Node的waitStatus进行跟踪维护。
看acquireQueued源码。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//7
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
//11
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
7-11 当前置节点为head,说明当前节点有权限去尝试拿锁,这是一种约定。如果tryAcquire返回true,代表拿到了锁,顺利撑场,函数返回。
13-15。if中包含两个方法,判断当前线程是否需要挂起等待。如果需要,那么就挂起,并判断外部是否调用线程中断。如果不需要,那么继续尝试拿锁。
18-19。如果try块中抛出非预期异常,那么取消当前线程获取锁的行为
有两点需要关注一下
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
若当前节点没有拿锁的权限或者拿锁失败,会进入方法看是否需要挂起(park),方法的参数是pred Node和当前Node的引用。
首先获取pred Node和waitStatus,若waitStatus为SIGNAL,说明前置节点也在等待拿锁,并且之后会唤醒当前节点。所以当前线程可以挂起休息,返回true。
如果shouldParkAfterFailedAcquire返回false,呢么再进行一轮重试。如果返回true,代表当前节点需要被挂起,则执行parkAndCheckInterrupt
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
是对当前线程进行挂起的操作。这里LockSupport.park(this);本质是通过UNSAFE下的native方法调用操作系统原语来讲当前线程挂起。
此时当前Node的线程将阻塞再此处,直到持有锁的线程调用release方法,release方法会唤醒后续节点、
通过对acquireQueued这个方法的分析,如果当前线程所在的系欸但那处于头节点的后一个,呢么它将不断去尝试拿锁,直到获取成功。否则需要挂起。这样就能保证head之后的一个节点在自旋CAS获取锁,其它线程都已经被挂起或正在挂起。这样就能够最大限度的避免无用的自旋消耗CPU。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
和tryAcquire一样,tryRelease也是AQS开放给上层自由实现的抽象方法。
在release中,假如尝试释放锁成功,下一步就要唤醒等待队列里的其它节点,这里主要看unparkSuccessor这个方法,参数是head 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;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
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);
}
获取head的waitStatus,如果不为0,那么将其置为0,表示锁以释放。接下来获取后续节点,如果后续节点为null或处于CANCELED状态,那么从后往前搜索,找到除了head以外最靠前且非CANCELLED状态的Node,对其进行唤醒,让它起来尝试拿锁。
这时拿锁,挂起,释放,唤醒都能够有条不紊且高效的执行。
为什么不直接从头开始搜索?
和addWaiter方法中,前后两个节点建立连接的顺序有关。
1 后节点的pre指向前节点。
2 前节点的next才会指向后节点。
这两步操作在多线程环境下并不是原子的,也就是说,如果唤醒是从前往后搜索,那么可能前节点的next还未建立好,那么搜索可能会中断.
自此AQS告一段落。