【并发编程】 --- Reentrantlock源码解析2:公平锁加锁过程超详细解析

谁说胖子不能爱 提交于 2020-04-14 13:22:40

【推荐阅读】微服务还能火多久?>>>

文章目录

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


1 前情回顾 — 同步方法交替执行时Reentrantlock公平锁的逻辑

前面一篇文章《【并发编程】 — Reentrantlock源码解析1:同步方法交替执行的处理逻辑》讲过无论是synchronized还是Lock锁,让某块代码变为同步的本质就是: 当一个线程执行该方法后,其他线程无法进入该方法,对应于Reentrantlock来说就是让其他线程的lock()方法无法正常返回。

那篇文章里讲过在方法交替执行时,Reentrantlock公平锁的主要逻辑如下,这里就不再过多叙述。
在这里插入图片描述


2 线程t1抢到锁并且没释放的情况


2.1 线程t2、t3、t4…入队 — addWaiter(Node.EXCLUSIVE), arg)方法

在这里插入图片描述
线程2的入队代码就这么一点,但是过程挺有意思的,这里总结如下:
在这里插入图片描述
当然这只是一种情况,即线程t2 成功排在了队列第2的位置。


其实应该还有一种情况: 即线程t2完成了上图中粉色字体的第(2)步,这时线程t3刚好进入addWaiter()方法,发现tail已经不为null了,这时候有可能t3会先排在队列第2的位置 ,这里就不画图了。。。

当然还有可能刚开始时一下有多个线程进入enq方法的情况,这里就不展开了。。。

—> 此时应该可以引申出一个思考: Reentrantlock所谓的公平锁,到底公平在哪???
—> 从这里我们可以看出来,这里所谓的公平并不是你先尝试获取锁,你就一定会最先获取到锁,而是你最先进入到了Node队列,你最先获取到锁!!! — 这一点我感觉也是非常非常重要。

要非常注意三点(即我在图中用红色字体写的三句话):

  • (1)只有被阻塞的线程才会进入Node链表进行排队 —> 也就是说获得锁的线程不会参与排队
  • (2)head节点的Thread永远为null —》 也就是Node链表中的第2个节点是最先进行排队的线程
  • (3)当一个线程发现前面有Node时,它会直接排在队尾 —》 也就是说当线程3、4。。刚进入addWaiter方法要排队时,直接就一个个往后排,不用再进enq方法了

本文后面的内容都假设t2的Node排在了队列第2的位置。


2.2 线程 t2、t3、t4…入队后 —》自旋 + park

其实这两点应该可以很容易的想到:

  • (1)前面入队只是为了让线程按照先后顺序被唤醒,但是要想阻塞线程,肯定还是得让其进行park
  • (2)但是因为线程的park和unpark肯定要涉及到用户态和内核态的来回切换,所以在真正的进行park之前先去自旋一下看一看前面的线程是不是已经释放锁了—> 这样就有可能不用真正的park了 。

该逻辑在前面讲JDK1.6对synchronized关键字的优化《【并发编程】 — synchronized锁的升级过程 + JDK1.6对synchronized关键字的其他优化简介 》时也讲到过 — 但由于没法调试,我们其实很难真正去了解它到底是怎么实现的。

接下来我们从源码角度看看Doug Lea大神的具体实现方式。


2.2.1 前置知识 — Node数据结构介绍

在介绍Doug Lea大神的具体实现方式之前先来介绍一下Node的具体数据结构,因为只有知道了里面的一个变量waitStatus,才能更好的明白其实现原理。
Node的结构如下:
**加粗样式**
概括起来可以分为三个部分


  • (1)线程的2种等待模式:

    • SHARED:表示线程以共享的模式等待锁(如ReadLock)
    • EXCLUSIVE:表示线程以互斥的模式等待锁(如ReetrantLock),互斥就是一把锁只能由一个线程持有,不能同时存在多个线程使用同一个锁
  • (2)线程在队列中的状态枚举:

    • CANCELLED:值为1,表示线程的获锁请求已经“取消”
    • SIGNAL:值为-1,表示该线程一切都准备好了,就等待锁空闲出来给我
    • CONDITION:值为-2,表示线程等待某一个条件(Condition)被满足 —》 相当于wait、notify/notifyAll的用法
    • PROPAGATE:值为-3,当线程处在“SHARED”模式时,该字段才会被使用上
    • 初始化Node对象时,默认为0
  • (3)成员变量:

    • waitStatus:该int变量表示线程在队列中的状态,其值就是上述提到的CANCELLED、SIGNAL、CONDITION、PROPAGATE
    • prev:该变量类型为Node对象,表示该节点的前一个Node节点(前驱)
    • next:该变量类型为Node对象,表示该节点的后一个Node节点(后继)
    • thread:该变量类型为Thread对象,表示该节点的代表的线程
    • nextWaiter:该变量类型为Node对象,表示等待condition条件的Node节点

注意: 只有未获得锁,或者说被阻塞的方法才会进入Node链表中 , 已经获得锁的线程肯定不会进入Node链表。


知道了Node的数据结构之后,还必须知道Reentrantlock的公平锁和和非公平锁都是独占锁 —》因此在不涉及到线程交互的情况下,公平锁的waitStatus只可能有三个值,即1 --- CANCELLED、-1 --- SIGNAL 和 0 ---初始化Node时的默认值。 —> 这一点务必要记住!


2.2.2 线程 t2、t3、t4…入队后 —》自旋 + park的具体流程

具体流程总结如下:
在这里插入图片描述


3 Reentrantlock公平锁加锁过程总结

自认为Reentrantlock公平锁的加锁过程被我这么一总结,还挺好理解的(☆_☆) ,为了更深刻一些,这里再做一个总结:
在这里插入图片描述
如果让我再用几句更简单的话来总结的话,我会这样进行总结:

  • (1)排队时,队首肯定是一个Thread为null的Node,其他线程的Node每次入队时都排在队尾(并维护双向链表关系)

  • (2)自旋 + park的过程 —》 如上图,哈哈哈!!!


end!

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!