ReentrantLock原理探析
最近在对之前看过的一些源码进行回顾,今天主要是对 ReentrantLock 的源码分析。ReentrantLock
有两个构造方法,当我们通过无参构造方法创建时,内部会创建的是非公平锁实例,那么本文也以非公平锁的实现机制做分析。
1 | public ReentrantLock() { |
加锁的方法其实很简单,只需要调用lock.lock();
就行了,接下来看看这个lock()
方法里到底执行了啥
1 | public void lock() { |
可以发现接着调用了NonfairSync
实例中的lock()
1 | final void lock() { |
先来看compareAndSetState(0, 1)
的逻辑
1 | protected final boolean compareAndSetState(int expect, int update) { |
这个U
是Unsafe
的实例,那这个类到底是干嘛用的呢,简单来讲,这个类就是用来提供硬件级别的原子操作的,比如通过它你可以获取到某个属性的内存地址。这个地方还牵涉到 CAS(Compare And Swap) 机制,包含三个操作数,内存地址 V, 预期原值 A,新值 B,进行 CAS 操作的时候首先会将内存地址 V 中的值与预期原值 A 比较,如果相同,则将 V 中的值更新为 B,不同则执行相应逻辑,像这里如果 CAS 操作失败的话会返回 false。 STATE
是什么呢?
1 | STATE = U.objectFieldOffset |
这个变量保存的就是state
成员变量的内存地址,这些都定义在NonfairSync
的父类AbstractQueuedSynchronizer
中,AbstractQueuedSynchronizer
是一个基于 FIFO 队列的同步框架,state
表示的就是同步状态,如果值为 0 说明没有线程获取锁,反之则说明有线程持有锁。ok,当compareAndSetState(0, 1)
返回true
时,即表示state
之前的值为 0,没有线程持有锁,当前线程获取锁成功,然后执行setExclusiveOwnerThread(Thread.currentThread());
把当前线程设置到exclusiveOwnerThread
变量中。我们再来看看返回false
的情况,执行acquire(1);
方法
1 | public final void acquire(int arg) { |
首先会调用tryAcquire(arg)
方法
1 | final boolean nonfairTryAcquire(int acquires) { |
这个方法是子类NonfairSync
来实现的,首先获取state
的值,如果是 0,表示之前持有锁的线程已经释放锁了,所以会尝试获取锁,反之不是 0 的话,会先判断当前线程是不是已经持有锁的线程,如果是的话 state 的值继续加 1,这里也说明ReentrantLock
是可重入锁。那么我们假如当前线程不是持有锁的线程,所以这个方法最后会返回false
,继续执行addWaiter(Node.EXCLUSIVE)
1 | private Node addWaiter(Node mode) { |
首先创建一个Node
实例,会持有当前线程的实例
1 | Node(Node nextWaiter) { |
然后是一个for
循环,里面会判断尾节点是不是null
,如果是null
的话,则会初始化队列,并创建一个新的节点,头尾节点都指向它
1 | private final void initializeSyncQueue() { |
继续执行循环中的代码,此时尾节点已经不为null
,会把之前持有当前线程节点的前驱指向尾节点,并把这个节点设置成尾节点,原来的尾节点的后驱指向含有当前线程的节点。总结一下就是如果当前没有队列,则创建一个新队列,头节点为没有设置任何值的Node
,尾节点为包含当前线程的Node
;如果已经有队列的话,则会把创建的含有当前线程的节点放入队列的尾部。当创建的Node
放入队列之后,我们再来看看acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
方法
1 | final boolean acquireQueued(final Node node, int arg) { |
循环中首先获取当前节点的前驱节点,判断前驱节点是否为头节点,如果是,执行tryAcquire(arg)
方法,尝试获取锁,这里假设仍有其它线程持有锁,那么会执行第二个 if 判断,shouldParkAfterFailedAcquire(p, node)
1 | private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { |
这个方法用来判断前驱Node
的waitStatus
,这里会把前驱节点的waitStatus
设置成Node.SIGNAL
,即等待获取锁的状态,设置成功会返回true
,继续执行parkAndCheckInterrupt()
1 | private final boolean parkAndCheckInterrupt() { |
线程到这里就被阻塞了。lock.unlock();
方法的执行流程
1 | public void unlock() { |
1 | public final boolean release(int arg) { |
来看看Sync
中tryRelease(arg)
的实现
1 | protected final boolean tryRelease(int releases) { |
首先判断c
的值是否为 0,如果不为 0,则表示返回false
,执行结束,这里也说明了如果之前lock()
方法被多次调用,那么unlock();
也应该的调用相等次数才会尝试释放锁,ok,那么假设这里之前只调用了一次lock()
方法,c 的值为 0,获取到head
节点,如果头节点不为空,并且waitStatus
不等于 0,执行unparkSuccessor(h);
1 | private void unparkSuccessor(Node node) { |
我们把头节点作为参数传入方法中,首先判断waitStatus
的值,之前我们设置过的等待获取锁的状态的值为-1
是小于 0 的,然后把头节点的waitStatus
的值通过 CAS 值设置为 0,获取头节点的后驱节点,即刚才我们为获取到锁的子线程节点,最后刚才阻塞的节点被唤醒,我们会到刚才阻塞住的地方
1 | final boolean acquireQueued(final Node node, int arg) { |
唤醒之后会继续执行for
循环里的逻辑,如果当前节点的前驱节点是节点,然后调用tryAcquire(arg)
方法就可以获取到锁了,setHead(node);
会把当前节点里面的前驱节点变量和当前持有线程的变量设置为null
,当前节点会变成head
节点。
非公平锁的逻辑大致就是如此了,ReentrantLock
里不是还有个公平锁FairSync
么,区别就在与非公平锁调用lock()
就会执行compareAndSetState(0, 1)
尝试获取锁,而公平锁FairSync
则会判断当前是否有线程持有锁,没有的话还会判断当前队列是否存在等待获取锁的节点,有的话会把当前线程的Node
放入队尾,一开始并不会去尝试获取锁。