JDK1.8中ReentrantLock源码分析

更新时间:2023-06-14 03:07:18 阅读: 评论:0

JDK1.8中ReentrantLock源码分析
前⼀篇博客简单介绍了ReentrantLock的定义和与synchronized的区别,下⾯跟随LZ的笔记来扒扒ReentrantLock的lock⽅法。我们知道ReentrantLock有公平锁、⾮公平锁之分,所以lock()我也已公平锁、⾮公平锁来进⾏阐述。⾸先我们来看ReentrantLock的结构:
从上图我们可以看到,ReentrantLock实现Lock接⼝,Sync与ReentrantLock是组合关系,且FairSync(公平锁)、NonfairySync(⾮公平锁)是Sync的⼦类。Sync继承AQS(AbstractQueuedSynchronizer)。在具体分析lock时,我们需要了解⼏个概念:
AQS(AbstractQueuedSynchronizer):为java中管理锁的抽象类。该类为实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供⼀个框架。该类提供了⼀个⾮常重要的机制,在JDK API中是这样描述的:为实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供⼀个框架。此类的设计⽬标是成为依靠单个原⼦ int 值来表⽰状态的⼤多数同步器的⼀个有⽤基础。⼦类必须定义更改此状态的受保护⽅法,并定义哪种状态对于此对象意味着被获取或被释放。假定这些条件之后,此类中的其他⽅法就可以实现所有排队和阻塞机制。⼦类可以维护其他状态字段,但只是为了获得同步⽽只追踪使⽤ getState()、tState(int) 和compareAndSetState(int, int) ⽅法来操作以原⼦⽅式更新的 int 值。这么长的话⽤⼀句话概括就是:维护锁的当前状态和线程等待列表。
CLH:AQS中“等待锁”的线程队列。我们知道在多线程环境中我们为了保护资源的安全性常使⽤锁将其保护起来,同⼀时刻只能有⼀个线程能够访问,其余线程则需要等待,CLH就是管理这些等待锁的队列。
CAS(compare and swap):⽐较并交换函数,它是原⼦操作函数,也就是说所有通过CAS操作的数据都是以原⼦⽅式进⾏的。
听叶lock()定义如下:
final void lock(){
冷天acquire(1);
}
lock()内部调⽤acquire(1),为何是”1”呢?⾸先我们知道ReentrantLock是独占锁,1表⽰的是锁的状态state。对于独占锁⽽⾔,如果所处于可获取状态,其状态为0,当锁初次被线程获取时状态变成1。
acquire()是AbstractQueuedSynchronizer中的⽅法,其源码如下:
public final void acquire(int arg){
if(!tryAcquire(arg)&&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
lfInterrupt();
}
从该⽅法的实现中我们可以看出,它做了⾮常多的⼯作,具体⼯作我们先晾着,先看这些⽅法的实现:
tryAcquire
tryAcquire⽅法是在FairySync中实现的,其源代码如下:
protected final boolean tryAcquire(int acquires){
//当前线程
final Thread current =Thread.currentThread();
//获取锁状态state
int c = getState();
/*
* 当c==0表⽰锁没有被任何线程占⽤,在该代码块中主要做如下⼏个动作:
* 则判断“当前线程”是不是CLH队列中的第⼀个线程线程(hasQueuedPredecessors),
* 若是的话,则获取该锁,设置锁的状态(compareAndSetState),
* 并切设置锁的拥有者为“当前线程”(tExclusiveOwnerThread)。
*/
if(c ==0){
if(!hasQueuedPredecessors()&&
compareAndSetState(0, acquires)){
tExclusiveOwnerThread(current);
return true;
}
}
/*
* 如果c != 0,表⽰该锁已经被线程占有,则判断该锁是否是当前线程占有,若是设置state,否则直接返回fal
*/
el if(current == getExclusiveOwnerThread()){
int nextc = c + acquires;
if(nextc <0)
throw new Error("Maximum lock count exceeded");
tState(nextc);
return true;
}
return fal;
}
在这⾥我们可以肯定tryAcquire主要是去尝试获取锁,获取成功则设置锁状态并返回true,否则返回fal。
hasQueuedPredecessors:”当前线程”是不是在CLH队列的队⾸,来返回AQS中是不是有⽐“当前线程”等待更久的线程(公平锁)。
public final boolean hasQueuedPredecessors(){
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next)==null|| s.thread !=Thread.currentThread());
}
Node是AbstractQueuedSynchronizer的内部类,它代表着CLH列表的⼀个线程节点。对于Node以后LZ会详细阐述的。
compareAndSetState:设置锁状态
protected final boolean compareAndSetState(int expect,int update){
pareAndSwapInt(this, stateOfft, expect, update);
}
compareAndSwapInt() 是sun.misc.Unsafe类中的⼀个本地⽅法。对此,我们需要了解的是 compareAndSetState(expect, update) 是以原⼦的⽅式操作当前线程;若当前线程的状态为expect,则设置它的状态为update。
tExclusiveOwnerThread:设置当前线程为锁的拥有者
protected final void tExclusiveOwnerThread(Thread t){
exclusiveOwnerThread = t;
}
addWaiter(Node.EXCLUSIVE)
private Node addWaiter(Node mode){
//new ⼀个Node节点
Node node =new Node(Thread.currentThread(), mode);
//CLH队列尾节点
Node pred = tail;
//CLH尾节点!= null,表⽰CLH队列 != null,则将线程加⼊到CLH队列队尾
if(pred !=null){
node.prev = pred;
if(compareAndSetTail(pred, node)){
<= node;
return node;
}
}
//若CLH队列为空,则调⽤enq()新建CLH队列,然后再将“当前线程”添加到CLH队列中。
enq(node);
return node;
}
addWaiter()主要是将当前线程加⼊到CLH队列队尾。其中compareAndSetTail和enq的源代码如下:
/**
* 判断CLH队列的队尾是不是为expect,是的话,就将队尾设为update
* @param expect
* @param update
夜晚盗汗* @return
*/
private final boolean compareAndSetTail(Node expect,Node update){
pareAndSwapObject(this, tailOfft, expect, update);
}
/**
* 如果CLH队列为空,则新建⼀个CLH表头;然后将node添加到CLH末尾。否则,直接将node添加到CLH末尾
* @param node
* @return
*/
private Node enq(final Node node){
for(;;){
Node t = tail;
if(t ==null){
if(compareAndSetHead(new Node()))
tail = head;
}el{
node.prev = t;
if(compareAndSetTail(t, node)){
<= node;
return t;
}
儿童蛀牙图片}
}
}
addWaiter的实现⽐较简单且实现功能明了:当前线程加⼊到CLH队列队尾。
acquireQueued
final boolean acquireQueued(final Node node,int arg){
boolean failed =true;
try{
//线程中断标志位
boolean interrupted =fal;
for(;;){
//上⼀个节点,因为node相当于当前线程,所以上⼀个节点表⽰“上⼀个等待锁的线程”
final Node p = node.predecessor();
/*
* 如果当前线程是head的直接后继则尝试获取锁
* 这⾥不会和等待队列中其它线程发⽣竞争,但会和尝试获取锁且尚未进⼊等待队列的线程发⽣竞争。这是⾮公平锁和公平锁的⼀个重要区别。                */
if(p == head && tryAcquire(arg)){
tHead(node);//将当前节点设置设置为头结点
<=null;
failed =fal;
return interrupted;
}
/* 如果不是head直接后继或获取锁失败,则检查是否要阻塞当前线程,是则阻塞当前线程
* shouldParkAfterFailedAcquire:判断“当前线程”是否需要阻塞
* parkAndCheckInterrupt:阻塞当前线程
*/
if(shouldParkAfterFailedAcquire(p, node)&&
parkAndCheckInterrupt())
interrupted =true;
}
}finally{
if(failed)
cancelAcquire(node);
}
}
在这个for循环中,LZ不是很明⽩为什么要加p==head,这篇博客有⼀个较好的解释如下:
p == head && tryAcquire(arg)
绿茶用英语怎么说
⾸先,判断“前继节点”是不是CHL表头。如果是的话,则通过tryAcquire()尝试获取锁。
其实,这样做的⽬的是为了“让当前线程获取锁”,但是为什么需要先判断p==head呢?理解这个对理解“公平锁”的机制很重要,因为这么做的原因就是为了保证公平性!
(a) 前⾯,我们在shouldParkAfterFailedAcquire()我们判断“当前线程”是否需要阻塞;
(b) 接着,“当前线程”阻塞的话,会调⽤parkAndCheckInterrupt()来阻塞线程。当线程被解除阻塞的时候,我们会返回线程的中断状态。⽽线程被解决阻塞,可能是由于“线程被中断”,也可能是由于“其它线程调⽤了该线程的unpark()函数”。
开始菜单
(c) 再回到p==head这⾥。如果当前线程是因为其它线程调⽤了unpark()函数⽽被唤醒,那么唤醒它的线程,应该是它的前继节点所对应的线程(关于这⼀点,后⾯在“释放锁”的过程中会看到)。 OK,是前继节点调⽤unpark()唤醒了当前线程!
此时,再来理解p==head就很简单了:当前继节点是CLH队列的头节点,并且它释放锁之后;就轮到当前节点获取锁了。然后,当前节点通过tryAcquire()获取锁;获取成功的话,通过tHead(node)设置当前节点为头节点,并返回。
总之,如果“前继节点调⽤unpark()唤醒了当前线程”并且“前继节点是CLH表头”,此时就是满⾜p==head,也就是符合公平性原则的。否则,如果当前线程是因为“线程被中断”⽽唤醒,那么显然就不是公平了。这就是为什么说p==head就是保证公平性!
在该⽅法中有两个⽅法⽐较重要,shouldParkAfterFailedAcquire和parkAndCheckInterrupt,其中
shouldParkAfterFailedAcquire:判断“当前线程”是否需要阻塞,源码如下:
劳动观念private static boolean shouldParkAfterFailedAcquire(Node pred,Node node){
//当前节点的状态
int ws = pred.waitStatus;
if(ws ==Node.SIGNAL)
return true;
if(ws >0){
do{
node.prev = pred = pred.prev;
}while(pred.waitStatus >0);
<= node;
趵突泉介绍
}el{
compareAndSetWaitStatus(pred, ws,Node.SIGNAL);
}
return fal;
}
waitStatus是节点Node定义的,她是标识线程的等待状态,他主要有如下四个值:
CANCELLED = 1:线程已被取消;
SIGNAL = -1:当前线程的后继线程需要被unpark(唤醒);
CONDITION = -2 :线程(处在Condition休眠状态)在等待Condition唤醒;
PROPAGATE = –3:(共享锁)其它线程获取到“共享锁”.
有了这四个状态,我们再来分析上⾯代码,当ws == SIGNAL时表明当前节点需要unpark(唤醒),
直接返回true,当ws > 0 (CANCELLED),表明当前节点已经被取消了,则通过回溯的⽅法(do{}while())向前找到⼀个⾮CANCELLED的节点并返回fal。其他情况则设置该节点为SIGNAL状态。我们再回到if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()),p是当前节点的前继节点,当该前继节点状态为SIGNAL时返回true,表⽰当前线程需要阻塞,则调⽤parkAndCheckInterrupt()阻塞当前线程。
parkAndCheckInterrupt:阻塞当前线程,并且返回“线程被唤醒之后”的中断状态,源码如下:
private final boolean parkAndCheckInterrupt(){
//通过LockSupport的park()阻塞“当前线程”。
LockSupport.park(this);
return Thread.interrupted();
}
从上⾯我们可以总结,acquireQueued()是当前线程会根据公平性原则来进⾏阻塞等待,直到获取锁为⽌;并且返回当前线程在等待过程中有没有并中断过。
lfInterrupt
private static void lfInterrupt(){
Thread.currentThread().interrupt();
}
lfInterrupt()产⽣⼀个中断。如果在acquireQueued()中当前线程被中断过,则需要产⽣⼀个中断。
Fairy lock()总结
我们再看acquire()源码:
public final void acquire(int arg){
if(!tryAcquire(arg)&&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
lfInterrupt();
}
⾸先通过tryAcquire⽅法尝试获取锁,如果成功直接返回,否则通过acquireQueued()再次获取。在acquireQueued()中会先通过addWaiter将当前线程加⼊到CLH队列的队尾,在CLH队列中等待。在等待过程中线程处于休眠状态,直到成功获取锁才会返回。如下:
⾮公平锁NonfairSync的lock()与公平锁的lock()在获取锁的流程上是⼀直的,但是由于它是⾮公平的,所以获取锁机制还是有点不同。通过前⾯我们了解到公平锁在获取锁时采⽤的是公平策略(CLH队列),⽽⾮公平锁则采⽤⾮公平策略它⽆视等待队列,直接尝试获取。如下:
final void lock(){
if(compareAndSetState(0,1))
tExclusiveOwnerThread(Thread.currentThread());
el
acquire(1);
}
lock()通过compareAndSetState尝试设置所状态,若成功直接将锁的拥有者设置为当前线程(简单粗暴),否则调⽤acquire()尝试获取锁;
acquire

本文发布于:2023-06-14 03:07:18,感谢您对本站的认可!

本文链接:https://www.wtabcd.cn/fanwen/fan/89/1037471.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:线程   状态   阻塞   获取
相关文章
留言与评论(共有 0 条评论)
   
验证码:
推荐文章
排行榜
Copyright ©2019-2022 Comsenz Inc.Powered by © 专利检索| 网站地图