什么是管程?
管理协调多个线程对共享资源的访问,是一种高级的同步机制。
有哪些管程模型?
hansen:唤醒其他线程的代码必须在当前线程的最后执行,以确保其他线程被唤醒时,当前线程已经执行完。
hoare:唤醒其他线程的代码可以在任意位置,且唤醒其他线程后,当前线程立即阻塞,当被唤醒线程执行完后,再继续执行当前线程。
mesa:唤醒其他线程的代码可以在任意位置,且被唤醒线程不会立即执行,而是加入一个队列,当当前线程执行完后,再从队列中获取并执行其他线程。(需要注意的是,由于存在时间差,所以真正执行的时候有可能已经不满足条件)
其中最常用的管程模型就是mesa,在java中实现的也是该模型。如下图是AQS的实现:
其实管程模型在java里的实现不止有AQS,synchronized 在jvm的底层实现像什么 cxq,entryList,waitSet也是mesa管程模型中的概念,同理object.wait()/object.notify()也是管程模型的实现。
所以具体什么是aqs?
aqs就是java代码对管程模型的一个抽象实现。把volatile state字段定义成共享资源,并且实现了同步等待队列和条件等待队列的入队/出队以及线程的阻塞/唤醒等公共操作,至于具体共享资源的获取/释放则交由各自实现类,不同的实现类可以定义不同的共享资源获取方式,由此可以实现公平锁/非公平锁,重入锁/不可重入锁,独占锁/共享锁等以满足不同的场景。
共享资源:
同步等待队列:
条件等待队列:
获取共享资源:
释放共享资源:
阻塞:
唤醒:
AQS最经典的实现莫过于 ReentrantLock。接下来看看是 ReentrantLock 如何通过AQS实现 lock/unlock 的。
AbstractQueuedSynchronizer.acquire
获取共享资源模板(aqs实现)
ReentrantLock .tryAcquire
共享资源获取逻辑(即ReentrantLock 自己实现的获锁方法)
在ReentrantLock中实现了公平/非公平两种获锁方式(默认为非公平)
以为非公平为例:
@ReservedStackAccess
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//cas修改共享资源
if (compareAndSetState(0, acquires)) {
//修改成表示获锁成功,设置当前线程
setExclusiveOwnerThread(current);
return true;
}
}
//可重入锁的实现
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
AbstractQueuedSynchronizer.addWaiter
获锁失败后入队同步队列(aqs实现)
private Node addWaiter(Node mode) {
Node node = new Node(mode);
for (;;) {
Node oldTail = tail;
if (oldTail != null) {
//cas加入队列
node.setPrevRelaxed(oldTail);
if (compareAndSetTail(oldTail, node)) {
oldTail.next = node;
return node;
}
} else {
//初始化队列
initializeSyncQueue();
}
}
}
AbstractQueuedSynchronizer.acquireQueued
入队后阻塞之前,根据前节点的状态进行一定次数的自旋获锁(aqs实现)
final boolean acquireQueued(final Node node, int arg) {
boolean interrupted = false;
try {
//这个循环保证一定会获取到锁
for (;;) {
final Node p = node.predecessor();
//如果前一个节点是头结点,再次尝试获锁
if (p == head && tryAcquire(arg)) {
//如果获锁成功就出队头结点
setHead(node);
p.next = null;
return interrupted;
}
//如果获锁失败或者前节点不是head的节点就根据前节点的状态来看是否需要阻塞
//需要阻塞就调用 LockSupport.park() 阻塞线程
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;
//如果前驱节点状态为 SIGNAL 直接返回 true 阻塞线程
if (ws == Node.SIGNAL)
return true;
//如果前驱节点状态大于0,说明线程已被返回,剔除无用节点前驱节点
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
}
//cas替换前驱节点状态为 SIGNAL
else {
pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
也就是说,获锁失败进入同步等待队列进行阻塞,而在实际阻塞之前会自旋再次尝试获锁。
实际上在自旋途中只有前节点是head的节点才会尝试获锁
如果前节点的 waitStatus 为 signal 则停止自旋
为避免无限自旋,自旋的同时会尝试修改前节点的状态为 signal
AbstractQueuedSynchronizer.release
释放共享资源模板(aqs实现)
h.waitStatus == 0 说明后面没有等待唤醒的节点
ReentrantLock .tryRelease
共享资源释放逻辑(即ReentrantLock 自己实现的释放锁方法)
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
//直接修改state值(不需要cas或者什么操作,因为释放锁不可能有并发)
setState(c);
return free;
}
AbstractQueuedSynchronizer.unparkSuccessor
唤醒节点(aqs实现)
private void unparkSuccessor(Node node) {
//清除node节点的waitStatus,重置为0
int ws = node.waitStatus;
if (ws < 0)
node.compareAndSetWaitStatus(ws, 0);
//如果node节点的后继节点被取消或者为空,就从尾部向前遍历找到实际的未取消后继节点。
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;
}
//LockSupport.unpark
if (s != null)
LockSupport.unpark(s.thread);
}
4、一开始没有线程获取锁,第一获取锁的线程进来直接获锁成功返回,没有入队操作,如何唤醒后继的线程?
答:虽然获锁线程没有入队,但是如果后续有等待线程需要用到队列的话还是会new一个node用于表示之前获锁线程的(相当于之前的获锁线程入队了)。
5、被唤醒的节点如何出队?