记录:ReentrantLock公平锁,判断是否需要排队时 h != t && (h.next == null || ···)的意义

本文详细解释了JDK1.8中ReentrantLock的锁竞争过程,特别是在AQS(AbstractQueuedSynchronizer)框架下的工作原理,特别关注公平锁规则可能导致的特殊情况,如空指针异常。
摘要由CSDN通过智能技术生成

JDK 1.8

当有新线程争抢ReentrantLock锁时,会判断锁的状态state =? 0
如果state == 0,进而判断自己是否需要排队等待

Reentranted.Syncprotected 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;
       }
   }
   ···
}
AQSpublic 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-2thread prev均置为null(头结点的特征)
    node-1next置为nullprev thread本身就是null),此时node-1没有被任何引用指向,自身引用也没有指向任何对象,会被GC。(node-1被临时引用指向了,所以能继续操作)
    – 此时AQS中只剩1个head头结点了,而且该节点的next = null(原本是tail节点,next = nulltail节点特征)
  • 假定此时有一个新的线程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 = nulltail != 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;
}

如有不对之处,还请见谅和指正,我会及时修改

写的比较简略,很多操作都没描述完全,相关代码也没贴上,不太适合没有基础的童靴

主要是给我自己看的,同时印证下自己的想法是不是对的,看了几篇相关的博客,怎么都想不通~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值