ReenTrantLock简介和源码导读

写在开头

  不得不说,和源码有关的笔记是真的不好写,主要源码中的很多方法互相依赖、频繁进出,导致文章的结构很难组织。尝试了多次之后,干脆决定按照源码的结构来组织吧。初学者不妨耐心读一读,虽然水平不见得多高,但是我想应该会有不小的帮助。

一、简介

  念经开始(其实不想看可以掠过):ReenTrantLock是java世界里比较常见的锁,是一种可重入的互斥锁。功能上来讲其实和更常用的synchronized差不多,但是功能更强大一些。

二、如何使用ReenTrantLock

  非常简单,如下面的代码示例:

// 创建ReentrantLock
Lock rtlock = new ReentrantLock(); 

// 线程中使用ReentrantLock
Thread thread = new Thread(()->{
	rtlock.lock();  // 加锁
	try {
		...  // 业务逻辑
	} catch (Exception e){
	} finally {
		rtlock.unlock();  // 释放锁
	}
})

thread.start();

要注意的是,业务逻辑最好用try catch包起来,防止业务逻辑的异常导致无法释放锁。

三、源码导读(根据代码结构组织子目录)

3.1、ReentrantLock的创建

通过new的方式可以创建实例,那就看下构造方法:
在这里插入图片描述
可以看到,ReentrantLock默认是非公平锁,但是也可以通过构造函数传参设置为公平锁:
在这里插入图片描述

3.2、加锁

即lock()方法,ReentrantLock通过sync.lock()来实现。逻辑并不复杂如下图:尝试通过CAS加锁(关于CAS本文不展开,简单来说是一种通过比较和更新变量"state"来加锁的方式),如果能够加锁成功(比如第一个执行任务的线程0),将变量state置1,同时将线程0设为当前的独占线程(“exclusiveOwnerThread”);否则,如果通过CAS加锁失败(比如其它同时执行任务的线程1)则放入队列去排队。这里插一句,下面代码中用到了“compareAndSetState”方法,其实类似“compareAndSetXXX”的方法后面还会看到很多次,都是利用CAS来保证线程安全:
在这里插入图片描述

3.2.1 compareAndSetState(0, 1)

这个方法就是通过CAS给当前线程加锁,CAS的原理这里不展开了,返回true说明获取锁成功,否则说明已有其它线程获取了锁。

3.2.2 acuire(1)

在acquire(1)方法中进行“排队”操作,参数1代表已有其它线程获取了锁。acquire方法中,当前线程会首先再尝试重新获取锁(说不定此时其它线程已经把锁释放了),可谓是贼心不死:
在这里插入图片描述

3.2.2.1 tryAcquire(arg)

顾名思义即“尝试获取锁”,如果成功拿到锁返回true,否则返回false。这个方法会进入到nonfairTryAcquire()方法中,这里面出现了“可重入”的概念:
在这里插入图片描述

3.2.2.2 tacquireQueued(addWaiter(Node.EXCLUSIVE), arg)

如果还是没有获取到锁,即添加到等待队列中:
在这里插入图片描述
第一个重点,addWaiter方法:
在这里插入图片描述

一眼望去,“node”、“prev”、“next”…当年刷题时的记忆开始攻击你,链表!它来了:首先new了一个node,传入两个参数:当前线程和mode(Node.Exclusive);然后尾节点tail赋值给pred(当你还在困惑变量名为什么是pred的时候,单词小王子的我已经知道了它是predecessor),此时tail还是null,因此会跳过条件语句执行enq(node),看一下enq(node):
在这里插入图片描述

第一次执行enq方法时需要创建head节点(例如下图的node1指向head),此后再执行enq方法,tail已经不为空了,后来的节点只需要把prev指向tail,再将tail的next指向这个节点即可(如下图的node2指向node1):
在这里插入图片描述
这个图里,node1和node2保存的就是正在排队的thread-1和thread-0。head节点似乎比较奇怪,从上面的代码来看它是个空节点,并没有包装任何线程,但其实可以把它理解为thread-0(即获取锁的那个线程)节点,因为thread-0已经在执行中了,不需要排队,所以它是个空节点。

第二个重点,排队逻辑(accuireQueued方法):
在这里插入图片描述

如果排队的是node1节点,则首先获取其前置节点p(也就是head节点),如果其前置节点不是head节点且此时仍获取不到锁(tryAcquire方法),则会执行shouldParkAfterFailedAcquire方法和parkAndCheckInterrupt方法。

3.2.2.2.1 shouldParkAfterFailedAcquire(p,node)

shouldParkAfterFailedAcquire方法中,会检测p节点的状态(waitStatus,有0,1,-1,-2,-3五个值)并对此节点做相应的处理:
在这里插入图片描述
这个方法里如果执行了最后的compareAndSetWaitStatus(pred, ws, Node.SIGNAL)方法,就会将p节点的状态置为-1(node1会把head的状态置为-1,而node2会把node1的状态置为-1),而整个accuireQueued方法是for(;;)循环的,因此下次再执行到这里的时候会返回true从而进入parkAndCheckInterrupt()方法。

3.2.2.2.2 parkAndCheckInterrupt()

非常简单,就是利用LockSupport.part将当前线程(下图中的“this”)挂起:
在这里插入图片描述

3.3、释放锁

即unlock()方法,ReentrantLock通过sync.release(1)来实现:
在这里插入图片描述

3.3.1、tryRelease(arg)

如图:
在这里插入图片描述

3.3.2、unparkSuccessor(head)

如下图:
在这里插入图片描述
这里有个问题要注意,执行整个unlock()是逻辑的线程拿着锁的thread-0,也就是说是thread-0执行了LockSupport.unpark方法唤醒了thread-1。而thread-1对应的node1,此刻正在前面提到过的parkAndCheckInterrupt方法中“被挂着”,唤醒后则会执行Thread.interrupted()并return:
在这里插入图片描述
return之后,其实thread-1还在accquireQueued方法的循环中,看看会发生什么:
在这里插入图片描述
在node1成为head节点之后,其后继节点即node2,在node1释放锁后,node2也遵循一样的逻辑,此处就不再重复描述了。

3.4、加锁和释放锁的完整流程图

在这里插入图片描述
在这里插入图片描述

3.5、附:waitStatus各值解释

/** 
* 表示当前线程被中断,可以被剔除 
*/
static final int CANCELLED =  1;

/** 
* 后继节点的线程处于等待状态,当前节点如果释放了锁或者被取消,会通知后继节点,从而使后继节点得以运行 
*/
static final int SIGNAL    = -1;
        
/** 
* 节点在等待队列中,节点的线程等待在Condition上,当其他线程对Condition调用了signal,该节点会从等待队列中转移
* 到同步队列中,加入到锁的获取中 
*/
static final int CONDITION = -2;
        
/**
* 表示下一次共享方式的同步状态获取将会被无条件传播下去
*/
static final int PROPAGATE = -3;

/**
* 不属于以上任何一个状态则为0,例如新new一个节点,此时waitStatus即为0
*/
0


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值