前言
在Java多线程环境中原子操作是保证线程同步的基本。在Java中通常使用所和循环CAS的方式实现原子操作。
使用循环CAS实现原子操作:在Java虚拟机中实现CAS操作是通过CMPXCHG指令实现的,而CAS循环就是循环的进行CAS操作直到成功为止。
使用锁机制实现原子操作:在Java中为并发编程的同步实现了很多锁机制,包括偏向锁、轻量级锁和互斥锁等。而锁机制保证了只有获取锁的线程才能操作锁定的内存区域,从而保证了线程同步。
在Java中提供了AbstractQueuedSynchronizer(队列同步器)来构建或者其他同步组件(比如:锁:ReentrantLock、ReentrantReadWrite和同步器:CountDownLatch、Semaphore)。接下来就分享一下队列同步器的具体实现。
队列同步器
所谓队列同步器,从名称中我们可以看出它提供了同步的作用,内部结构依赖于队列。
关于同步
AbstractQueuedSynchronizer提供了一个框架,实现阻塞锁和相关的同步器。队列同步器主要通过继承的方式被使用,子类通过实现队列同步器中的抽象方法来管理同步状态以及实现同步功能。
队列同步器的状态管理方法:
- setState():设置当前同步状态;
- getState():获取当前同步状态;
- compareAndSetState():使用CAS操作设置当前同步状态,CAS操作可以保证设置状态的原子性;
队列同步器的同步状态获取方法:在获取锁时分为两种方式——独占式和共享式。独占式获取锁就是当前只允许一个线程获取同步状态,而共享式获取锁允许多个线程获取到共享状态(具体可以允许多少个线程获取到同步状态取决于设置的同步状态,这个稍后还会提到)
继承同步队列器需要实现的方法:
- tryAcquire(int arg):独占式获取同步状态时需要重写改方法,在该方法中查询当前状态并判断同步状态是否符合条件,然后通过compareAndSetState()方法设置同步状态;
- tryRelease(int arg):独占式释放同步状态,释放后在等待队列中的线程就可以继续获取同步状态;
- tryAcqurieShared(int arg):共享式获取同步状态,返回值大于0时表示获取成功,这也是上面提到的允许多少个线程获取同步状态取决于同步状态值得设置;
- tryReleaseShared(int arg):共享式释放状态;
队列同步器中获取同步状态的模板方法:
- acquire(int arg):独占式获取同步状态,如果当前线程获取同步状态成功,就从该方法返回,否则加入同步队列。该方法会调用tryAcquire()方法;
- acquireInterruptibly(int arg):作用与acquire()方法相同,区别是该方法在当前线程被中断时返回;
- tryAcquireNanos(int arg):该方法表示在给定时间内没有获取到同步状态就中断返回;
- release(int arg):独占式释放同步状态,该方法调用tryRelease();
- acquireShared(int arg):共享式获取同步状态,该方法会调用tryAcquireShared()方法。与acquire()的区别就是该方法允许多个线程获取同步状态;
- acquireSharedInterruptibly(int arg):作用与acquireShared()方法相同,区别是该方法在当前线程被中断时返回;
- tryAcquireSharedNanos(int arg):该方法表示在给定时间内没有获取到同步状态就中断返回;
- releaseShared(int arg):独占式释放同步状态,该方法调用tryReleaseShared();
从上面的方法可以看出同步队列器中的获取同步状态主要分为:独占式获取与释放同步状态、共享式获取与释放同步状态。
关于队列
在同步队列器中通过一个队列来实现同步状态的管理,这个队列是FIFO双向队列。如果当前线程获取同步状态失败时就会加入到同步队列中,然后进行自旋式的获取同步状态。
在同步队列器中有Node类实现队列。
static final class Node {
/** 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; //此节点当前处于条件队列中.在传输之前,它不会用作同步队列节点,此时状态将设置为0.当其他节点对其调用single()时,该节点将加入同步队列.
static final int PROPAGATE = -3; //releaseShared应该传播到其他节点。 在doReleaseShared中设置(仅限头节点)以确保继续传播,即使其他操作已经介入。
volatile int waitStatus; // 等待状态
volatile Node prev; //链接到当前节点/线程的前任节点
volatile Node next; //链接到当前节点/线程的后继节点
volatile Thread thread; //当前线程
Node nextWaiter; //链接到等待条件的下一个节点
/**
* Returns true if node is waiting in shared mode.
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
/** Establishes initial head or SHARED marker. */
Node() {}
/** Constructor used by addWaiter. */
Node(Node nextWaiter) {
this.nextWaiter = nextWaiter;
THREAD.set(this, Thread.currentThread());
}
/** Constructor used by addConditionWaiter. */
Node(int waitStatus) {
WAITSTATUS.set(this, waitStatus);
THREAD.set(this, Thread.currentThread());
}
/** 通过CAS操作设置等待状态. */
final boolean compareAndSetWaitStatus(int expect, int update) {
return WAITSTATUS.compareAndSet(this, expect, update);
}
/** 通过CAS操作设置后继节点 */
final boolean compareAndSetNext(Node expect, Node update) {
return NEXT.compareAndSet(this, expect, update);
}
/** 通过CAS操作设置前驱节点 */
final void setPrevRelaxed(Node p) {
PREV.set(this, p);
}
// VarHandle mechanics
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); //CAS操作关联后继节点
PREV = l.findVarHandle(Node.class, "prev", Node.class); //CAS操作关联前驱节点
THREAD = l.findVarHandle(Node.class, "thread", Thread.class); //CAS操作关联当前线程
WAITSTATUS = l.findVarHandle(Node.class, "waitStatus", int.class);//CAS操作关联等待状态
} catch (ReflectiveOperationException e) {
throw new Error(e);
}
}
}
可以看到在同步队列器的队列节点(Node)中保存了获取同步状态失败的线程、等待状态、前驱节点和后继节点以及相关属性等。同时队列同步器中还有两个Node字段,head、tail它们分别表示队列的头节点和尾节点。其中head节点是获取到同步状态的节点。
队列同步器的具体实现
经过上面的分析,我们知道队列同步器通过内部的同步队列来管理各个线程获取同步状态的过程。这个过程有分为独占式获取和共享式获取。下面对这两种方式分贝进行分析。
独占式同步状态的获取和释放
当线程需要获取同步状态时需要调用队列同步器的acquire()方法。
接下来讲一下独占式同步状态的获取过程。
/** 获取同步状态的入口 */
public final void acquire(int arg) {
// 独占式获取同步状态tryAcquire()方法只能返回0或1因为只有一个线程可以获取同步状态
if (!tryAcquire(arg) &&
// 获取同步状态失败时,将当前线程加入到同步队列中
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
}
/** 创建一个独占模式的节点 */
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();
}
}
}
final boolean acquireQueued(final Node node, int arg) {
boolean interrupted = false;
try {
// 当前线程开始自旋并阻塞该线程直到获取同步状态
for (;;) {
final Node p = node.predecessor(); // 获取前驱节点
if (p == head && tryAcquire(arg)) { //只有头结点才能尝试获取同步状态,符合FIFO队列规则
setHead(node); // 获取到同步状态,将当前节点设为头节点
p.next = null; // help GC
return interrupted; //从acquire()方法返回
}
//检查节点是否中断
if (shouldParkAfterFailedAcquire(p, node))
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
cancelAcquire(node);
if (interrupted)
selfInterrupt();
throw t;
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//SIGNAL状态时判断当前线程是否已经中断,调用parkAndCheckInterrupt()方法
return true;
if (ws > 0) { waitStatus>0表示已取消(CANCLLED状态),将从同步队列中移出
do {
node.prev = pred = pred.prev; //从同步队列中移出CANCLLED状态的节点
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//waitStatus初始状态为0,也可能是PROPAGATE,那么将其设置为SIGNAL,表示可以向后继节点传播状态
pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
}
return false;
}
经过上面的源码分析可以总结独占式获取同步状态的流程,如下:
- 线程获取同步状态,获取成功,返回;否则,进行下一步。
- 将线程生成队列节点,然后加入到同步队列的尾部,线程进入自旋并且阻塞。
- 如果当前线程的节点的前驱节点是头节点,尝试获取同步状态,获取成功,则将该节点设置为头结点。否则进入第2步。
当线程执行完同步代码逻辑时,还需要释放获取到的锁,然后其他线程才能继续获取同步状态。接下来就分析一下独占式释放同步状态的过程。
public final boolean release(int arg) {
//释放同步状态
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 唤醒后续节点
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
//状态小于0说明当前节点不是取消状态,则将状态设置为0
int ws = node.waitStatus;
if (ws < 0)
node.compareAndSetWaitStatus(ws, 0);
// 后续节点
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node p = tail; p != node && p != null; p = p.prev)
if (p.waitStatus <= 0)
s = p;
}
if (s != null)
// 唤醒后续个节点
LockSupport.unpark(s.thread);
}
通过分析独占式获取和释放同步状态的过程,可以知道在获取同步状态的时候,队列同步器在内部维护一个双向队列,如果同步状态获取失败,就将线程生成节点加入到队列中,并且进行自旋,直到自己的头结点时队列头部(head节点)时,此时说明已经有了获取同步的机会,然后开始尝试获取同步状态。如果获取成功,就设置为head节点。当释放同步状态时,释放成功后从队列移出并且唤醒后续节点。
独占式超时获取同步状态
独占式超时获取状态就是在给定时间内如果获取同步状态失败就将当前节点从队列中移出。通过调用队列同步器的doAcquireNanos()方法来实现独占式超时获取同步状态。
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout; //超时时间
final Node node = addWaiter(Node.EXCLUSIVE); //将独占式节点加入队列
try {
for (;;) {
// 自旋过程和独占式一致
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return true;
}
nanosTimeout = deadline - System.nanoTime(); // 获取剩余时间
// 如果剩余时间结束,取消自旋
if (nanosTimeout <= 0L) {
// 取消自旋,并将节点移出队列
cancelAcquire(node);
return false;
}
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > SPIN_FOR_TIMEOUT_THRESHOLD)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
/** 取消获取同步状态 */
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
node.thread = null; // 将自旋线程置null
// Skip cancelled predecessors
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev; // 移出队列
Node predNext = pred.next;
node.waitStatus = Node.CANCELLED; // 将状态设置为"取消状态"
// If we are the tail, remove ourselves. 如果是尾节点说明队列中只剩一个节点
if (node == tail && compareAndSetTail(node, pred)) {
pred.compareAndSetNext(predNext, null);
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && pred.compareAndSetWaitStatus(ws, Node.SIGNAL))) &&
pred.thread != null) {
//需要唤醒后继节点
Node next = node.next;
if (next != null && next.waitStatus <= 0)
pred.compareAndSetNext(predNext, next);
} else {
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
可以看出独占式超时获取同步状态指示在原有的基础上加入了一个时间限制的逻辑,当自旋过程中如果达到了给定的时间,那么久取消获取同步状态并且江北移出队列。
到这里独占式获取同步状态和释放线分析到这里,接下节说一下共享式获取和释放同步状态。
共享式同步状态的获取和释放
共享式获取和独占式获取最主要的区别就在于同一时刻是否可以有多个线程获取到同步状态。
通过调用队列同步器的qcquireShared()方法可以共享式的获取同步状态。
public final void acquireShared(int arg) {
// 获取同步状态
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg); // 获取失败的操作
}
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED); //将节点加入同步队列
boolean interrupted = false;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) { // 节点为头节点时可以尝试获取同步状态
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r); // 获取成功设置为头结点
p.next = null; // help GC
return;
}
}
if (shouldParkAfterFailedAcquire(p, node))
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
} finally {
if (interrupted)
selfInterrupt();
}
}
可以看出,共享式获取同步状态失败时也要将节点加入同步队列中,其他的自旋过程和独占式时一致的。这里有一个问题,共享式获取同步状态到低能够支持多少个线程同时获取同步状态呢?
我们可以看到在共享式的获取同步状态时,获取成功或失败的条件是这样的 -> if (tryAcquireShared(arg) < 0)。也就是说当同步状态(state)小于0时获取状态失败。这里给出个例子:当设置同步状态为3时,当一个线程获取同步状态成功时将同步状态减1,那么就可以有3个线程同时获取到同步状态。
和独占式一样,共享式也需要释放同步状态。在共享式释放同步状态时调用releaseShared()方法。
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0)) //设置状态直到成功,这个过程确保多个线程的情况下也能全部释放成功
continue; // loop to recheck cases
unparkSuccessor(h); // 唤醒后继节点
}
else if (ws == 0 &&
!h.compareAndSetWaitStatus(0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
共享式释放状态成功后也会唤醒后继节点。
总结
到这里算是把队列同步器的实现原理分析了一遍。不过还有其他的获取同步状态的方式没有讲到,感兴趣的同学可以阅读源码。
通过学习队列同步器(AbstractQueuedSynchronizer)可以了解到锁和其他同步器的实现基础。我们可以看到如果想完整的实现锁或者同步器的功能,还需要实现队列同步器提供的抽象方法,然后才能实现自己想要的功能。总的来说队列同步器就是给我们提供了一个线程同步工具的模板,可以使我们方便的实现我们想要的同步工具的功能。