Synchronizer
.
铺垫了这么多,终于到了我们的Synchronizer了。其实jdk的很多阻塞工具都是基于一个通用的类构建的例如ReentrantLock,FutureTask,Semaphore,CountDownLatch等等,这个就是AbstractQueuedSynchronizer。AQS也是面试当中很容易问到的一环。
ReentrantLock,FutureTask,Semaphore,CountDownLatch这些类都需要的是通过自身的状态来判断是否阻塞当前线程,或者是唤醒被阻塞的线程。
1.ReentrantLock#lock(),如果当前的锁未被其他线程占有,或者是点用着本身已经占有了锁那么lock方法直接返回,否则阻塞。ReentrantLock#Unlock()则是根据当前线程是否已经不占有锁来释放那些阻塞在Lock方法里的线程。
2.CountDownLatch#await(),则是根据自身的一个次数是否是0来决定是否阻塞当前线程,而他的CountDownLatch#countDown(),则是根据自身次数是否是0来决定是否唤醒阻塞的线程。
所以AQS提供了一个int值state来表示状态的抽象,并提供getState,setState,compareAndSetState等方法供子类自己去控制或是判断自身状态,提供tryAcquire(独占),tryAcquireShare(共享) 利用返回值决定是否阻塞调用线程,提供与之对应的tryRelease,tryReleaseShare 利用返回值决定是否唤醒被阻塞的线程。
下面我们先介绍一个独占模式AQS用法。
public class SimpleLatch {
private Sync sync = new Sync();
public void pass() {
sync.acquire(1);
};
public void open() {
sync.release(1);
};
private static class Sync extends AbstractQueuedSynchronizer {
/**
*
*/
private static final long serialVersionUID = 1L;
@Override
protected boolean tryAcquire(int arg) {
// state 0表示关闭 1表示打开 默认关闭
return getState() == 1;
}
@Override
protected boolean tryRelease(int arg) {
setState(1);
return true;
}
}
}
测试代码
public void testSimple() {
SimpleLatch simpleLatch = new SimpleLatch();
for (int i = 0; i < 1; i++) {
new Thread(() -> {
simpleLatch.pass();
System.out.println(Thread.currentThread().getName() + " Im passed!");
}).start();
}
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("begin to open");
simpleLatch.open();
}
结果
begin to open
Thread-0 Im passed!
SimpleLatch 是一个简单的二元闭锁。只有当状态为1时才允许线程通过,使用的是AQS提供的独占模式锁。
下面看看AQS的源码他是怎么实现的。
AQS独占模式
1.获取
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
首先判断是否能够获取状态,可以的话直接返回。
private Node addWaiter(Node mode) {
//实例化node对象,设置当前线程和独占模式。
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;
}
//尾节点为空 自旋把头,尾节点指向一个新实例化的节点,自旋向后面添加入参节点
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;
}
}
}
}
获取失败时加入同步队列,如果同步队列没有实例化时实例化同步队列。之后加入同步队列,node中包含当前线程,waitState=0。(其中多次使用CAS自旋保证操作一定成功)。
final boolean acquireQueued(final Node node, int arg) {
//是否抛出异常
boolean failed = true;
try {
//是否被打断
boolean interrupted = false;
for (;;) {
//获取前驱节点
final Node p = node.predecessor();
//如果前驱节点是头结点 并且可以获取状态时
if (p == head && tryAcquire(arg)) {
//把头结点踢出队列
setHead(node);
p.next = null; // help GC
设置本节点为头结点,设置操作成功,返回是否中断
failed = false;
return interrupted;
}
//判断当前线程是否需要休息
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取前驱节点状态
int ws = pred.waitStatus;
//如果前驱节点等待被唤醒 则返回true,外层方法阻塞当前线程
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
//前驱节点为cancel 那么循环删除前面所有的cancel节点。
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 {
// 0 或者 PROPAGATE,0则是当前节点还没来的及设置前驱节点状态,
// PROPAGATE则是共享模式中可能出现的情况
// 本线程设置前驱节点状态为SIGNAL。
/*
* 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);
}
//返回false 要求再次自旋。
return false;
}
private final boolean parkAndCheckInterrupt() {
//阻塞当前线程
LockSupport.park(this);
//线程已被唤醒,检查是否被中断,此方法会清除中断标志位,所以在acquire方法中进行selfInterrupt()来补偿。
return Thread.interrupted();
}
2.释放
public final boolean release(int arg) {
//如果释放成功
if (tryRelease(arg)) {
//获取头结点
Node h = head;
//头结点部位空,并且状态不等0时(也就是状态==-1)
if (h != null && h.waitStatus != 0)
//注意此时头结点的后继节点已经设置前驱节点为SINGAL,
//也就是说后继节点的线程可能已经阻塞,所以需要唤醒后继节点
unparkSuccessor(h);
//waitStatus ==0 也就是说后继节点还活着 还有一次tryAcquire的机会,所以不需要唤醒。
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
//设置入参节点的状态为0,可能会与后继节点的修改前驱节点操作产生并发,不过没关系。
//这里设置失败也没所谓
/*
* 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);
}
.
所以说独占模式的话,一个线程调用release只会释放同步队列中第一个等待的线程,所以在SimpleLock的实现中,加入有多个线程调用pass,但是只有一个线程调用了open()的话,那么只有第一个线程会被释放,别的线程就尴尬了。
好了,下一篇我们就再去描述下一非独占模式的AQS实现,也就是一个release可以释放多个阻塞的线程。