AQS结构最最最简略流程整理
AQS是Lock的根底,之前一向想自己整理一下,怎样办一向没有静下心来整理,这里记录并做整理,文章根据JDK 1.8。
独占锁的上锁流程
public class MutexCustom implements Lock {
public static final int STATUS_ORIGIN = 0;
public static final int STATUS_TARGET = 1;
private Sync sync;
public MutexCustom() {
sync = new Sync();
}
public static class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
if (compareAndSetState(STATUS_ORIGIN, STATUS_TARGET)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
setState(STATUS_ORIGIN);
setExclusiveOwnerThread(null);
return true;
}
@Override
protected boolean isHeldExclusively() {return true;}
public Condition newCondition() {return new ConditionObject();}
public int getCurrState() {return getState();}
}
@Override
public void lock() {sync.acquire(STATUS_TARGET);}
@Override
public void lockInterruptibly() throws InterruptedException {sync.acquireInterruptibly(STATUS_TARGET);}
@Override
public boolean tryLock() {return sync.tryAcquire(STATUS_TARGET);}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(STATUS_TARGET, unit.toNanos(time));
}
@Override
public void unlock() {sync.release(STATUS_TARGET);}
@Override
public Condition newCondition() {return sync.newCondition();}
public int getState() {return sync.getCurrState();}
}
咱们先来关注最根底的最根底的最根底的锁的功能,也便是acquire获取锁
和release开释锁
acquire获取锁
public final void acquire(int arg) {
// Node.EXCLUSIVE为null
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这里有三个主要的办法:
- tryAcquire测验获取锁,得到true,也便是获取到锁直接回来,也便是不会走里边的办法体,直接履行临界区的代码了。
- tryAcquire回来false,履行addWaiter,望文生义,把自己加到等候的nodeList最后面
- acquireQueued
首要清晰锁是怎样收效的,假如这3个办法都很快的回来了成果,不管回来的是true仍是false,都会持续往下履行,差异只不过是执不履行selfInterrupt()办法算了,并没有影响当时线程持续往后履行后续办法,也便是临界区代码。
假如这三个办法里边有某一个办法没有很快的回来成果,而是把自己困在了一个while
或许for(;;)
循环里边,或许运用LockSupport.park()
办法,那么这个线程就堵塞住了,不能持续往后履行后续的临界区代码,那这个时分就完成了堵塞。
在合理的逻辑下竞赛和开释这个堵塞的代码就完成了锁。
那究竟是在哪个办法里边堵塞住了呢?咱们挨个办法去看:
1.1. tryAcquire
// 需求被重写,给出获取资源逻辑
// 比方最简略的互斥锁,只需求在state为0的时分compareAndSetState(0,1)成功回来true就好了。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
这个没有什么好说的,已然AQS是完成Lock的根底结构,那肯定就要把逻辑代码空出来让开发者自己去完成,回来true表明竞赛到了锁,能够履行临界区代码,反之没有竞赛到锁,需求等候锁的开释并重新竞赛。
1.2. addWaiter
private Node addWaiter(Node mode) {
// mode假如是Node.EXCLUSIVE则为null,so,node的next现在为null
Node node = new Node(Thread.currentThread(), mode);
// 测验快速刺进到最尾端,也便是把新创建的node放到tail的next,然后用node取代tail。
// tail是全局变量,初始化为null
Node pred = tail;
// 前提是tail不为空
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 假如tail为空则直接走enq办法。
enq(node);
return node;
}
// 其实比快速刺进不一样的便是假如没有tail,把tail赋值成head,然后再持续append
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
// 初始化状况tail为null,给head设置一个new Node(),一起把tail也指向这个head
// tail\head同为全局变量,初始化都为null
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
看着注释理一遍:
addWaiter
办法望文生义,便是把自己的node刺进到链表的最尾端,加入抢占资源的排队序列,等候抢占资源。
需求留意的是,tail / head
都是全局变量,都是初始化为null,刚开始运转的时分tail / head
都为null,假如tail为null,会走enq
办法,做了两件事
- tail为null,先把head初始化为一个
new Node()
,一起暂时把tail赋值为head。 - 把当时node赋值为head的next,一起当时node作为tail。
留意回来值是t,而不是当时node,也便是回来node的preNode,是t,上述情况下当时链表有两个值head->tail,回来的应该是是head而不是tail。当然这里调用enq
办法并没有运用它的回来值。addWaiter
办法回来值是当时node。
所以从空链表初始化完毕后,链表里应该有两个节点,而不是只有一个head节点,保证了工作节点能获取到一个pre节点。
compareAndSetHead和compareAndSetTail这些CAS办法都不是堵塞的,都是Unsafe办法完成,所谓乐观锁,不必关怀脏数据,只需关怀成功或许失利。
1.3. acquireQueued
这里附上一个Node状况的表格:
类型 | 值 | 阐明 |
---|---|---|
CANCELLED | 1 | 当时节点由于超时或许中止被撤销,节点进入这个状况以后将坚持不变。 注:这是唯一大于0的值,许多判别逻辑会用到这个特征 |
SIGNAL | -1 | 后继节点会依据此状况值来判别自己是否应该堵塞、当时节点的线程假如开释了同步状况或许被撤销、会告诉后继节点、后继节点会获取锁并履行(当一个节点的状况为SIGNAL时就意味着其后置节点应该处于堵塞状况) |
CONDITION | -2 | 当时节点正处在等候行列中,在条件达到前不能获取锁。 |
PROPAGATE | -3 | 当时节点获取到锁的信息需求传递给后继节点,共享锁模式运用该值 |
INITIAL | 0 | 节点初始状况,默认值,表明当时节点在sync同步堵塞行列中,等候着获取锁 |
// 放入等候行列
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)) {
// 假如前一个节点是head而且这时获取到了临界资源,把当时node设置为head,head的next置为null
// 两个条件:
// 1. node是head的next节点,也便是轮到它了。
// 2. tryAcquire(arg)得到了资源,也便是获取到了锁。
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 留意哈,这里是&&,也便是shouldParkAfterFailedAcquire办法回来true才会履行parkAndCheckInterrupt,两个办法一起回来true才会履行interrupted = true
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
// 检测失利的时分是否需求堵塞自己,我要睡觉了,等人来叫醒我再检测是不是需求排队。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 留意,ws是上一个节点的status
int ws = pred.waitStatus;
// SINGAL: 此node的后续node经过park把堵塞住了,所以当时node在release或许撤销acquire的时分有必要unpark一下后续的node。
// (当一个节点的状况为SIGNAL时就意味着其后置节点应该处于堵塞状况)
if (ws == Node.SIGNAL)
// 前置节点现已设置了SIGNAL,能够堵塞我的线程了,后续检测到Node.SIGNAL会唤醒我的线程
return true;
if (ws > 0) {
// 前置节点被设置为cancel了,越过pre,我来当pre, ^_^
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 检测到自己是INITAL或许PROPAGATE,把pre的status设置为SIGNAL,持续循环
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
//park睡觉了,睡醒了之后检测是否需求中止。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
注释很清晰了,整理一下:
acquireQueued
办法望文生义,测验获取锁,获取不到就进入行列等候。
- acquire处在一个for(;;)死循环中, 获取锁需求两个条件:
1.1. 前置节点是head,也便是轮到我自己获取锁了
1.2. tryAcquire(arg)==true,也便是竞赛到了锁 - 没必要一向测验获取锁,把自己标记为需求唤醒,一起调用park堵塞,告诉predecessor,需求的时分unpark我。
LockSupport.park(this);
之后当时线程会堵塞,直到有unpark操作才会持续往后履行。所以在for(;;)死循环中并没有一向测验获取锁,在获取不到之后会堵塞自己,并把node的状况标记为SIGNAL
。后续看到node的状况为SIGNAL
,在自己的锁开释后会调用unpark唤醒自己持续履行for(;;)中没有履行完的部分,持续测验tryAcquire获取锁,这时就能获取到锁并完毕循环了。
这便是最根底的获取锁的功能。
release开释锁
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
// head不为空,而且status不是初始状况,则需求唤醒unpark一下下一个节点。不然下个节点还没有堵塞,不需求unpark唤醒。
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
// 需求unpark唤醒下一个节点
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 需求unpark的Thread被当时node的继任node持有,通常情况下是下一个node,但是有可能是cancel状况或许是null,
// 从尾部逆序往前遍历以找到状况不是canceled的继任者。
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);
}
开释锁的流程看起来就简略了一些。tryRelease
成功开释锁之后,找到下个节点,唤醒他。
至于为什么需求从后往前遍历而不是从前往后遍历:
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刺进新的next节点,运用CAS保证安全,假如当时线程履行到了②,但是这时有一个新线程履行到了①,新线程只履行了把node的pre指向了tail,这时tail的next仍然为null,这时tail变了再履行CAS就会失利了。
这种情况下从后往前遍历是能够找到的,所以是安全的。
参阅链接:
www.zhihu.com/question/50…
blog.csdn.net/anlian523/a…
blog.csdn.net/java_lifeng…