【并发编程】 --- Reentrantlock源码解析4:公平锁加锁过程中 [判断当前线程是否要排队的具体细节] 超详细解析

源码地址:https://github.com/nieandsun/concurrent-study.git


1 简单回顾

【并发编程】 — Reentrantlock源码解析1:同步方法交替执行的处理逻辑
【并发编程】 — Reentrantlock源码解析2:公平锁加锁过程超详细解析
【并发编程】 — Reentrantlock源码解析3:公平锁释放锁过程超详细解析

上面三篇文章我自认为 已经非常详细的介绍了Reentrantlock公平锁加锁和解锁的过程 —》 其实非公平锁比公平锁更简单,其主要区别就在于在获取锁时:

  • 如使用公平锁,当前线程即使发现锁没有被占用,也不能直接去抢锁,它要先去看一看有没有线程在排着队等着获取锁
  • 若使用非公平锁 ,当前线程若发现锁没被占用,它就会立刻去获取锁

有兴趣的可以自己撸一下非公平锁的源码。

本篇文章将主要来讲解一下,在使用公平锁时,当前线程在发现锁没有被占用的情况下,在高并发环境中判断自己是否需要排队的具体实现细节。


2 源码 + 宏观分析

上篇文章《【并发编程】 — Reentrantlock源码解析3:公平锁释放锁过程超详细解析》其实已经介绍过,Doug Lea大神对这块逻辑的具体实现代码其实就寥寥数行,源码如下:

public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    //注意1:下面的逻辑并不是原子操作,这一点我想大家肯定不会有任何疑问
    //注意2:当下面的表达返回false时表示当前线程不用排队
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

为了更好的理解,这里顺便将尝试获取锁的逻辑和上诉源码一起贴出:
在这里插入图片描述
那我们接下来详细的看一下,这几行代码,到底都考虑了哪些情况。


这段代码的具体执行流程如下。 通过该图我们可以捋出有且仅有的两条不用排队的线路:

  • h != t 不成立时
  • h != t 成立,但是s = h.next) == null 和 s.thread != Thread.currentThread()两个条件都不成立时

接下来我就从这两条路线来讲解这块代码具体考虑的情况。
在这里插入图片描述


3 高并发环境下当前线程使用公平锁时判断自己是否要排队的具体实现细节

3.1 h != t


3.1.1 h != t 不成立时(即h等于t时) —> 不用排队的原因

所谓h != t 不成立时不用排队,也就是说,当 h == t 时,不用排队!!! 而其实 h == t 对应了如下两种情况。


3.1.1.1 情况1 —> h和t都等于null —> 不用排队

这很容理解,当h和t都等于null,说明Node链表都还没初始化呢,此时肯定没有排队要获得锁的线程,因此当前线程就肯定不用排队了。

其实可以很容易的想到:同步方法交替执行时,就是这种情况。这时候有兴趣的可以再来看一下我之前写的文章 《【并发编程】 — Reentrantlock源码解析1:同步方法交替执行的处理逻辑》 就更可以理解为什么Reentrantlock在同步方法交替执行时,效率会很高了:

  • (1) 既没有park和unpark线程 —》 肯定也就不会调用内核函数
  • (2)甚至连Node链表都不用初始化

3.1.1.2 情况2 —> h和t不等于null,但h=t的情况 —> 不用排队

其实这也很容易理解,h和t既然不等于null,而h和t又相等,那Node链表就只有如下一种情况了。也就是说此时虽然Node链表已经初始化了,但是还没有线程入队到第2 个节点 —> 那就肯定还没有线程排队,那当前线程肯定也就不用排队了!
在这里插入图片描述


3.1.1.3 小结 — h = t 为啥就直接可以说不用排队了???

其实通过2.2.1和2.2.2已经可以知道,当h = t的时候:

  • 要么根本就没有Node链表
  • 要么虽然有Node链表,但是还没有一个线程在Node链表里进行排队

这里我觉得一定要联系一下我在《【并发编程】 — Reentrantlock源码解析2:公平锁加锁过程超详细解析》这篇文章里说的一句话:Reentrantlock公平锁所谓的公平并不是你先尝试获取锁,你就一定会最先获取到锁,而是你最先进入到了Node队列,你最先获取到锁!!!

因此当h=t时,当前线程肯定就不用排队了!!!


3.1.2 h != t 成立时 —> 是否需要排队仍未可知原因分析

这里首先要明确一下,通过前面几篇文章的知识可知:任何情况下都不可能出现h和t一个为null,一个不为null的情况

因此当判定h != t 成立时的那一刻Node链表里肯定至少有一个线程正在排队了!!!

那当前线程肯定就有可能也要排队了。

当然在判断h != t 成立的那一刻,当前线程也有可能不需要排队 —> 原因请看3.3!!!

因此总得来说h != t这个判断条件成立时对于当前线程来说,是否需要排队,还未可知!!!


3.2 (s = h.next) == null 能被运行的情况


3.2.1 (s = h.next) == null 成立时为什么需要排队

能读懂这个逻辑,必须要有多线程并发的意识!!!

这里首先说一下为什么 (s = h.next) == null有可能会成立??? 原因如下:

在这里插入图片描述


接下来说一下为啥当 (s = h.next) == null成立时就必须要排队

由上图来说,该条件如果成立,那肯定说明此前排在第2的线程已经获得 或者说或得过锁了!!!

  • 当然如果该线程正在持有锁,那当前线程肯定就得老老实实去排队了 — 这一点肯定是毋庸置疑的!!!
  • 但是我想肯定有人会像我一样去挑刺: 诶,有可能此前排在第2的线程就是这么牛逼,它不仅获得到了锁,而且还咔咔咔咔执行完,释放了锁 —> 这时当前线程是不是就不用再去排队了??? —> 对,非常对!!!但是呢?肯定没必要再去为此搞个逻辑啊
    • 首先来说这种情况出现的几率肯定本来就小
    • 其次此时当前线程就肯定排在Node链表中第2的位置了 — 那它就极有可能通过2次自旋获取到锁了,或许再为此单独实现一个逻辑,还不如自旋来的效率高呢!!!

3.2.1 (s = h.next) == null 不成立时 —> 是否需要排队仍未可知原因分析

最后说一下为啥当 (s = h.next) == null不成立时无法判断是否需要排队

  • 首先该条件不成立,就说明肯定在此刻至少有一个线程正在排队 —》 因此当前线程也有可能需要排队
  • 其次 当前线程也有可能不需要排队 —> 原因请看3.3!!!

因此总得来说 (s = h.next) == null这个判断条件不成立时对于当前线程来说,是否需要排队,还未可知!!!



3.3 s.thread != Thread.currentThread() 能被运行的情况


3.3.1 s.thread != Thread.currentThread() 成立时需要排队的原因

s.thread != Thread.currentThread() 成立其实也可以分为两种情况:

  • (1)排在第2的线程还在Node链表中,但不是当前线程 —》 那当前线程需要排队肯定是毋庸置疑的
  • (2)进行该条件判断时排在第2的线程已经获得 或者说或得过锁了!!! —》 这种情况我就不具体分析了,其实可以参考我在3.2.1 中的描述 —》 总而言之这种情况下肯定也是需要排队的!!!

3.3.2 s.thread != Thread.currentThread() 不成立时不需要排队的原因 ★★★

我觉得只有看明白了这里,你才算真真正正明白了Reentrantlock!!!

s.thread != Thread.currentThread() 不成立 —> 也就是说Node链表中排在第2的线程就是当前线程!!!

首先再看一次这整块逻辑的流程图:
在这里插入图片描述
然后我们仔细想一想,究竟怎样才有可能会出现s.thread != Thread.currentThread()可以被执行,但执行结果却为false的情况??? —》想破头皮,你会发现,其实就有且仅有一种情况:

拥有锁的线程刚刚释放锁,排在Node链表中第2的线程通过自旋,尝试获取锁!!!

当然自旋的时机有可能会有如下两种:

  • (1)拥有锁的线程刚刚释放锁,而排在Node链表第2的线程刚刚完成了入队,还处在自旋当中
  • (2)拥有锁的线程刚刚释放锁,并唤醒排在Node链表第2的线程 —》 由此引发排在第2的线程尝试获取锁!!!

4 总结

高并发环境下当前线程使用公平锁时,当发现锁没被占用时,判断自己不需要排队的情况有且仅有三种:

  • (1)根本就没有队伍 — Node链表根本就没初始化
  • (2)有队伍,但队伍中只有一个Node — Node链表队首肯定是一个线程为null的Node,没有第2个Node,说明还没有线程在排队
  • (3)拥有锁的线程刚释放锁,第二个Node自旋时的情况

其实现在想想也挺简单的(☆_☆)


5 本篇文章背后故事及启发

一开始写这篇文章时,我是按照如下三个条件由上往下逐个分析的,但是文章写到最后一个条件时,发现自己的理解有些竟然是错误的!!!

  • (1)h != t
  • (2) (s = h.next) == null
  • (3) s.thread != Thread.currentThread()‘

很伤心!!!

究其原因,就是因为我一开始就钻进了细枝末节里,而没有从宏观的角度去想一下,上面三个条件都被执行的情况下到底是个什么情况!!! — 我当时竟然以为与重入锁有关,现在想想,真是可笑!!!

—》 由此得到如下启发:

分析问题,一定要先从宏观上认清这个东西究竟是要干什么!!!只有搞明白了其具体的方向,才不会在细枝末节里迷失自己!!!


end !!!

发布了230 篇原创文章 · 获赞 333 · 访问量 54万+

猜你喜欢

转载自blog.csdn.net/nrsc272420199/article/details/105452712
今日推荐