共享标记位state=0 表示资源是空闲的;state=1表示有1个线程获取到资源,如何独占模式,判断持有锁的线程是否是当前线程,若是,则state变为2,达到可重入性
如果获取锁失败立即返回,则不需要入队
如果需要不断的尝试,业务侧可循环适用用tryLock不断重试
定义了(共享锁、独占锁)加锁、释放锁的骨架
Queues:队列
Synchorizer: 同步阻塞
用于记、判断共享资源正在被占用的情况
为什么适用int而不是boolean?
为了可重入(reentrantLock多次加锁,state需要一致递增,最后需要对称释放锁)
为了共享锁模式
头尾节点:线程没有获取到锁,需要排队
int waitStatus 等待状态 1, -1, -2, -3
以下方法是骨架,实际的实现由子类完成
获取锁
public final void acquire(int arg) {
// 尝试获取锁
if (!tryAcquire(arg) &&
// addWaiter加入等待队列 acquireQueued自旋尝试获取锁,一直获取不到park住
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
获取锁失败,则进入等待队列
private Node addWaiter(Node mode) {
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;
}
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}问题: compareAndSetTail 是原子的,但是整个if语句不是原子的,为什么不会出问题?
compareAndSetTail是通过原子的方式设置尾节点(此时包含了tail=node:将当前的node节点设置尾tail),pred.next = node;只是设置之前的tail节点的next节点,这个操作不会有其他线程干扰
问题: 为什么要使用 Node pred = tail?
因为tail节点的指针会被其他线程修改,类似写时复制,如果tail节点改变,pred的指针还是指向原来的tail节点
问题: 为什么要先尝试快速入队,而不是直接入队
应该是测试的结果,认为tryAcquire失败,大概率尾节点不为空,先尝试入队,如果成功,则少了一步完整队逻辑中的 tail为null的判断
完整的入队(循环cas直到入队成功)
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;
}
}
}
}
自旋尝试获取锁(自旋时间中, FIFO中的等待队列可能执行完,这样当前节点就会成为最靠前的节点,能够获取到锁)
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 获取当前节点的上一个节点
final Node p = node.predecessor();
// 当前任务之前不是head,重新循环
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 判断是否需要挂起,避免过度自旋,消耗CPU
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
acquire完整的流程
public final boolean release(int arg) {
// 尝试释放锁
if (tryRelease(arg)) {
Node h = head;
// 唤醒等待的线程
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);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
// 从后往前找第一个 waitStatus <=0的
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
加锁、入队、出队、挂起、唤醒
问题:为什么从后往前找?
enq入队方法,从tail尾入队,compareAndSetTail将当前节点设置为tail后,FIFO的倒数第二个节点 ->tail还没有创建链接(t.next = node),如果从前往后找,可能会漏掉此节点。这也是为什么 node.prev = t 在 compareAndSetTail 之前的原因
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
public final void acquireShared(int arg) {
// 尝试获取共享锁
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
private void doAcquireShared(int arg) {
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);
}
}
待续