ReentrantLock源码分析
概述
ReentrantLock意为重入锁,它是基于AQS实现的一种独占锁,在java中另一个可重入独占锁就是我们常见的synchronized关键字,拥有这两种锁的线程都能重复进入获取锁的对象的代码或者方法中不被阻塞,并且是独占被锁资源的。同时ReentrantLock提供了synchronized所没有的许多特性,并且提供了更好的性能。当我们需要对并发做出更为细致的控制时,可以选择使用ReentrantLock来替代synchronized。
公平锁和非公平锁
- 公平锁是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,按顺序打饭。
- 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程先获取锁,完全取决于cpu的调度
ReentrantLock是通过内部类Sync的两个子类FairSync(公平锁)和NonfairSync(非公平锁)来实现线程获取锁的。其中默认的构造函数创建的就是非公平锁。可以通过ReentrantLock另一个构造函数传入true来构造公平锁。
1 | public ReentrantLock() { |
下面我们看一下这这两个锁的父类Sync的类图
可以发现Sync最终是继承AbstractQueuedSynchronizer,它内部的实现完全是基于AQS实现的,同时从ReentrantLock的类注释上我们可以得知它是一种类似synchronized关键字的独占重入锁。并且从内部类Sync的两个子类以及其重写AQS的tryAcquire以及tryRelease方法我们更加可以断定ReentrantLock是以独占模式去获取和释放锁的。由于ReentrantLock的默认构造函数是以非公平锁的方式运行,接下来我们就先从非公平锁的代码入手,一步一步分析其内部的原理。
NonfairSync
1 | static final class NonfairSync extends Sync { |
NonfairSync加锁是在其父类的nonfairTryAcquire方法中实现的,接下来我们分析一下这个方法
nonfairTryAcquire
1 | final boolean nonfairTryAcquire(int acquires) { |
nonfairTryAcquire方法尝试获取锁是通过非公平的方式去获取锁,为什么是非公平的方式呢?因为这里线程第一次获取锁本质上是通过compareAndSetState方法cas设置state的值,也就是说存在多个线程去竞争,假设a,b,c三个线程按顺序都执行到了compareAndSetState(0, acquires)
这行代码,但是最终哪个线程能够获取到锁完全取决于cpu的调度,结果是不明确的,这就是非公平性的体现,获取到锁的顺序并不是申请锁的顺序。
FairSync
1 | static final class FairSync extends Sync { |
FairSync和NonfairSync在加锁的过程中唯一有区别的地方就在于线程第一次获取锁的处理方式上,NonfairSync完全是由cpu调度取决哪个线程能够获取锁,而NonfairSync在此基础上添加了一个前提,当前队列中没有其它线程等待时间超过当前线程才使当前线程尝试去获取锁,这就是公平性的体现,就像排队取饭一样,谁先来的(等待的时间最长)就能够获取锁,后来的(等待的时间短)需要排队。这里我们只需要重点关注AQS中的hasQueuedPredecessors方法是如何判断是否有线程排队时间超过当前线程的。
hasQueuedPredecessors
1 | public final boolean hasQueuedPredecessors() { |
将主要判断逻辑拆分成if语句后的形式如下
1 | public final boolean hasQueuedPredecessors() { |
当这个方法返回true时表明存在排队时间超过当前线程的线程。也就是要满足h != t
这个条件,将整个判断逻辑翻译成大白话就是如果此刻头部节点和尾部节点不相同并且头部节点的下一个节点为null(说明头部节点刚刚设置完成或者下一个节点刚刚设置到尾部还没来得及将头部节点的next指向该节点)或者下一个节点中的线程不是当前线程(表明其它线程在当前线程之前就已经排队了),就代表当前队列中存在排队时间超过当前线程的线程。同时在FairSync中的tryAcquire方法中获取锁的第一个if判断中if (c == 0)
这个判断已经提早避免了一些由于线程调度发生的意外情况了。
unlock
在上面公平锁和非公平锁的分析中我们已经了解了ReentrantLock是通过其内部类重写AQS的tryAcquire方法然后配合AQS实现独占锁逻辑的,而解锁是不存在公平和非公平性的,ReentrantLock的解锁同样是依赖AQS实现的。
1 | public void unlock() { |
解锁的时调用的是内部类Sync的release方法,而Sync继承了AQS,本质上还是调用了AQS的解锁方法。AQS的release方法调用的是子类重写的tryRelease方法,它会根据该方法返回值来判断是否需要唤醒下一个等待的线程。
1 | protected final boolean tryRelease(int releases) { |
tryRelease方法根据AQS中state值减去releases值剩余的值来判断当前线程是否还拥有锁,等于0说明能够解锁,否则说明当前线程还存在重入。由于独占锁特性,不需要原子性的操作setState和setExclusiveOwnerThread方法。
例子
1 | package com.example.demo; |
ReentrantLock使用需要保证加锁后一定能够被解锁,否则其它线程就会一直阻塞无法被唤醒。通常配合finally关键字使用,保证加锁后一定能够被解锁。
总结
ReentrantLock是一种独占可重入锁,提供了公平锁和非公平锁两种锁的特性。默认实现是非公平锁。内部加锁和解锁是依赖AQS实现的。两种锁内部类都继承了另一个名为Sync的继承了AQS的类,重写了tryAcquire和tryRelease方法。对AQS熟悉的话那么理解ReentrantLock完全是没有难度的。