JDK 1.8
当有新线程争抢ReentrantLock
锁时,会判断锁的状态state =? 0
如果state == 0
,进而判断自己是否需要排队等待
Reentranted.Sync:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
···
}
AQS:
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}
hasQueuedPredecessors()
中有一个h != t && ((s = h.next) == null || ···)
判断逻辑
h != t
说明AQS中存在有排队的节点,那么可以“直接”返回true
,告知当前线程需要排队··········································【你先别急↓↓↓】
当前线程就会创建一个node
节点,入队AQS,然后阻塞
有一种情况:h != t
但是无需排队,可直接获取锁
- 假定
thread-1
当前获取了锁,此时state = 1
thread-2
由于state = 1
,会把thead-2
封装到node-2
节点中,入队AQS。假定AQS中没有节点(head == null && tail == null
)
–node-2
入队AQS前,会new Node()
作为AQS的头结点(thread = null
prev = null
)
–node-2
入队AQS时,更新AQS状态:head = 头结点
保持不变,tail = node-2
···
–node-2
本质是AQS中的第2个节点,也可以理解为第一个排队的节点(头结点thread = null
)
– 此时thead-1
释放了锁,置state = 0
node-2
入AQS后,会有一次自旋操作 —— 尝试再次获取锁(因为这期间可能锁已经被释放了)- 由于
state = 0
,可能获取锁成功,下一步是判断“自己”是否需要排队hasQueuedPredecessors()
–h != t
意味着AQS中至少有2个节点,至少有1个排队等待获取锁的节点
–s.thread != Thread.currentThread()
AQS中第2个节点中存放的thread-id就是当前线程,即当前线程本身就是AQS中第1个排队的节点
– 由于公平锁的规则,只有第1个排队等锁的节点才能获取当前锁,因此当前线程直接获取锁
h != t && s.thread != Thread.currentThread()
在一个特殊场景下,会出现问题
- 假定AQS中有2个节点,其中1个是头结点,记为
head = node-1
;另1个是排队节点,记为node-2
;当前获得锁的是thread-1
线程 - 当
thread-1
释放锁时,置state = 0
,唤醒AQS中的第1个排队节点node-2
(也是唯一一个排队节点,即尾结点) node-2
被唤醒后,会从阻塞的地方继续往下执行,此时会先获取到锁,然后更新AQS状态
–node-2
充当AQS新的头结点head = node-2
–node-2
的thread
prev
均置为null
(头结点的特征)
–node-1
的next
置为null
(prev
thread
本身就是null
),此时node-1
没有被任何引用指向,自身引用也没有指向任何对象,会被GC。(node-1
被临时引用指向了,所以能继续操作)
– 此时AQS中只剩1个head
头结点了,而且该节点的next = null
(原本是tail
节点,next = null
是tail
节点特征)- 假定此时有一个新的线程
thread-3
来争抢锁,明显会争抢失败,又由于AQS中已经有了一个头结点(同时也是尾结点),那么只会把自己封装为node-3
,待入队AQS - thread-3需要更新AQS状态
–node-3
前驱指向tail
节点
–node-3
作为新的tail
节点
–head.next = node-3
(即新的尾节点)-----------------------------------------问题出在这里
– 倘若thread-3
在更新head.next
时,分配的CPU-2核的时间片正好用完,此时head.next = null
,tail != null
,即head != null
– 此时thread-2
在CPU-1核上完成了自己的操作,释放了锁;thread-4
分配到CPU-2核的时间片,获取锁,由于state = 0,进而判断自己是否要排队
– 此时h != t
,再判断另一种无需排队直接获取锁的情况s.thread != Thread.currentThread()
,此时发生空指针异常
– 因此在进行s.thread != Thread.currentThread()
判断前,需要先判断h.next =? null
– 如果h != t && h.next == null
,说明出现了上述情况,当前节点直接排队
– 如果h != t && h.next != null
,进而判断s.thread != Thread.currentThread()
条件
AQS:thread-3更新AQS的源码
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;
}
如有不对之处,还请见谅和指正,我会及时修改
写的比较简略,很多操作都没描述完全,相关代码也没贴上,不太适合没有基础的童靴
主要是给我自己看的,同时印证下自己的想法是不是对的,看了几篇相关的博客,怎么都想不通~