【并发编程】 --- Reentrantlock源码解析2:公平锁加锁过程超详细解析
文章目录
- 1 前情回顾 --- 同步方法交替执行时Reentrantlock公平锁的逻辑
- 2 线程t1抢到锁并且没释放的情况
- 3 Reentrantlock公平锁加锁过程总结
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!