目录
写在开头
不得不说,和源码有关的笔记是真的不好写,主要源码中的很多方法互相依赖、频繁进出,导致文章的结构很难组织。尝试了多次之后,干脆决定按照源码的结构来组织吧。初学者不妨耐心读一读,虽然水平不见得多高,但是我想应该会有不小的帮助。
一、简介
念经开始(其实不想看可以掠过):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