Java并发AQS原理分析(二)
上一篇详细的分析了独占模式下如何对线程进行处理:简单的总结是Java面向用户提供了锁的机制,后面的实现使用了一个同步队列,由于队列具有先进先出的特点,把每个线程都构造成为队列中的节点,每个节点定义一个状态值,符合状态的节点(线程)才可以有执行的机会,执行完释放,后面的线程只能是等待着前面的执行结果进行判断,每个线程的执行都是独立的,不能有其他的线程干扰,所以在用户的角度来看线程是在同步的执行的,并且是独占式的。
共享式和独占式的区别主要是能否在同一时刻不同的线程获取到同步状态
上图可以很直观的看出独占式和共享式的区别。在对一个资源进行访问的时候,对读操作是共享式的,而对写操作是独占式的。
下面接着分析共享模式下线程之间是怎么实现的?
依然找到方法执行的入口,在上一篇我们找到了这几种方式的顶层方法。
共享模式同步状态的获取:
在acquireShared()
方法中,调用了tryAcquireShared()
返回一个状态值,进行判断,获取成功直接返回,失败进入等待队列中。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
方法执行的顺序:
- 调用
tryAcquireShared()
方法尝试去获取资源,具体实现是在子类中进行实现,成功直接返回,失败执行下面的方法。 - 调用
doAcquireShared()
方法,线程进入等待队列,等待获取资源。
tryAcquireShared()
方法是在子类中实现的,这里不需要讨论,但是返回值已经是定义好的。方法返回一个int类型的值,当返回值大于0的时候,表示能够获取到同步状态。
doAcquireShared()方法:
在前面分析过的方法这里不再分析
private void doAcquireShared(int arg) {
//调用addWaiter方法把线程加入队列尾
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);
}
}
基本上和独占式的处理方式一致,根本的体现就是在下面的这个setHeadAndPropagate()
方法
setHeadAndPropagate():
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
setHead(node);
//如果还有资源剩余,继续唤醒后面挨着的线程
if (propagate > 0 || h == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
在独占模式下获取资源,即使资源还有剩余,队列中的后一个线程节点也不会被唤醒执行,只有自己占有才是独占,而共享式的是有剩余就会给后面。
parkAndCheckInterrupt():
private final boolean parkAndCheckInterrupt() {
//让当前对象停止执行,阻塞状态
LockSupport.park(this);
//检查中断
return Thread.interrupted();
}
cancelAcquire():
private void cancelAcquire(Node node) {
if (node == null)
return;
node.thread = null;
//获得前驱结点
Node pred = node.prev;
//如果状态值大于0
while (pred.waitStatus > 0)
//移除当前节点的前驱结点
node.prev = pred = pred.prev;
//设置节点状态值为CANCELLED
node.waitStatus = Node.CANCELLED;
//如果是尾节点,设置尾节点
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
int ws;
//当节点既不是尾节点,也不是头节点的后继节点时,下面的这些判断其实执行的出队操作起作用的就是compareAndSetNext()方法将pred指向后继节点
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
//如果node是head的后继节点,则直接唤醒node的后继节点
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
共享模式同步状态的释放:
尝试释放资源如果成功,会唤醒后续处于等待状态中的节点
public final boolean releaseShared(int arg) {
//尝试释放资源
if (tryReleaseShared(arg)) {
//唤醒后继节点
doReleaseShared();
return true;
}
return false;
}
doReleaseShared():
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
//唤醒后继节点
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head)
break;
}
}
和独占式的释放从代码上看他们的区别是共享式的释放方法中有一个循环。
引用:https://www.cnblogs.com/waterystone/p/4920797.html
上面方法的流程也比较简单,一句话:释放掉资源后,唤醒后继。跟独占模式下的release()相似,但有一点稍微需要注意:独占模式下的tryRelease()在完全释放掉资源(state=0)后,才会返回true去唤醒其他线程,这主要是基于独占下可重入的考量;而共享模式下的releaseShared()则没有这种要求,共享模式实质就是控制一定量的线程并发执行,那么拥有资源的线程在释放掉部分资源时就可以唤醒后继等待结点。例如,资源总量是13,A(5)和B(7)分别获取到资源并发运行,C(4)来时只剩1个资源就需要等待。A在运行过程中释放掉2个资源量,然后tryReleaseShared(2)返回true唤醒C,C一看只有3个仍不够继续等待;随后B又释放2个,tryReleaseShared(2)返回true唤醒C,C一看有5个够自己用了,然后C就可以跟A和B一起运行。而ReentrantReadWriteLock读锁的tryReleaseShared()只有在完全释放掉资源(state=0)才返回true,所以自定义同步器可以根据需要决定tryReleaseShared()的返回值。
在共享模式下,资源有剩余就会给后面的邻居,不会自己占有,这是和独占式的根本区别。