前言
ReentrantLock这个类,相信大家多多少少在项目里都会去用到这个类,但我相信大部分人都没去研究过源码。我在这里把我学习这个类的一些经验和心得分享出来,希望对大家有所帮助,水平有限,文章中有错误的地方也请不吝指正,共同进步。
学习基础
这个类依旧是根据aqs框架去实现的,如果不知道什么是aqs的,可以去看看我写的aqs的学习笔记。独占锁和共享锁,aqs是基础,关于aqs的部分,本文将不再赘述。
引导
关于ReentrantLock的源码解析,百度上没有一万个人写,也至少有八千个人写,大家随便百度一篇原创,我相信大家都会有很大收获。所以这篇文章,我想从产品设计的角度去猜测还原Doug Lea是如何开发出来ReentrantLock这个类的。
功能规划
我们假设自己就是Doug Lea,当前jdk是1.4版本。我们需要在源码里提供一个非常方便的加锁的工具类,名字已经想好了,就叫ReentrantLock,目的是开发者在使用的时候可以很方便的进行加锁,解锁。而此时java中只有一个synchronized关键字。既然我们要开发一个新的工具类,我们肯定是希望比synchronized更好用,覆盖的场景更广泛。所以我们首先分析synchronized的缺点(1.4版本的缺点):
- synchronized加锁的代价太大。
- synchronized不支持操作部分线程,释放锁的时候等同于唤醒全部。
- synchronized毫无公平性可言,我们需要公平,公平,还是TMD公平。
- 锁的粒度太粗,一个实例的多个synchronized方法不能同时被多个线程访问。
- synchronized在阻塞的过程中不可被中断。
- synchronized超时时会一直锁住,甚至会造成死锁。
设计
第一点:
早期synchronized获取不到锁就会被阻塞,而阻塞线程需要cpu从用户态转换到内核态,这是一个很消耗资源的操作,所以我们需要开发一个api级别的框架,这个框架就是aqs,aqs利用cas自旋和阻塞,多次cas获取不到资源的时候再去阻塞,被唤醒的线程再使用cas去获取资源,不是单纯的cas自旋或者阻塞,这样就同时兼顾了性能和效率。这样,我们开发了一个aqs框架作为ReentrantLock的基础,来解决synchronized加锁代价大的问题。同时也提供了一个可扩展的aqs框架。
(synchronized以前确实是很笨重, 但是在各个版本优化之后,现在的性能和ReentrantLock是相差无几的,官方建议是如果synchronized能实现你的需求,那么尽量使用synchronized,具体的优化大家可以百度一下。)
第二点:
我们想自定义规则让部分线程去争抢资源,让部分线程不去争抢资源。我们需要使用到aqs的node节点链表。但是我们知道,node队列是轮询唤醒全部,没有对线程做区分,我们只想要操作部分线程。于是我们想到一种方法:
我们根据我们的定义,把线程归类,然后把归好类的线程放到一个个单独的队列里,我们需要操作哪部分线程, 就把这部分线程所在的队列的所有线程,放到aqs队列里,剩下的事情我们交给aqs,这样我们就做到了操作部分线程,而不影响其他线程。
第三点
我们需要设计一个公平锁,但是非公平锁也肯定要有,因为非公平锁性能更好,而且大部分人可能并不关心公平性。所以我们的想法是在构造方法里放入一个Boolean参数,用户想要公平锁就传入true,想要非公平锁就传入false,同时在代码里对公平和非公平分开实现。
第四点
synchronized锁粒度太粗,这跟synchronized的实现机制有关,因为synchronized是jvm去实现的,是对对象和类的操控。所以synchronized方法获取的是对象锁或者类锁,当synchronized锁住实例或者类对象的时候,其他线程再去获取会被阻塞。而我们设计了ReentrantLock,这样我们需要几把锁,在我们的类里定义几个成员变量就可以了,各个ReentrantLock对象之间互不影响,这样这个问题也就不解自解了。
第五点
synchronized在阻塞的过程中是忽略中断的,只有在获取到锁的时候才会去响应中断。为了解决这个问题我们需要在ReentrantLock设计一个可以检测中断的lock方法, 我们暂且命名为lockInterruptibly()。
aqs中分为cas和阻塞唤醒两个过程,我们可以在自旋cas中加入检测中断的逻辑,这样我们在获取锁的部分过程中就做到了检测中断。
第六点
synchronized代码块中,如果碰到一段代码偶尔需要执行1分钟或者两分钟,那其他的线程获取锁等待的时间就太长了,那显然不是我们能接受的,所以我们在ReentrantLock设计一个根据我们设定时间去获取锁的操作,超过我们期望的时间就不再去获取锁, 直接返回结果。
设计工作已经做完了,我们接下来就是把我们的需求翻译成代码。
实现公平非公平(copy)
公平锁和非公平锁,区别只有在tryAcquire()方法上,所有会有很多可以共用的代码。为了代码美观,我们先写一个公平锁和非公平锁都可以继承的基类,这个类就是Sync,Sync的功能依赖aqs,所以我们要继承aqs。
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
/**
* Performs {@link Lock#lock}. The main reason for subclassing
* is to allow fast path for nonfair version.
*/
abstract void lock();
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
// 非公平锁获取资源
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
// 释放资源
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
// 判断当前占用线程是否是当前线程
protected final boolean isHeldExclusively() {
// While we must in general read state before owner,
// we don't need to do so to check if current thread is owner
return getExclusiveOwnerThread() == Thread.currentThread();
}
final ConditionObject newCondition() {
return new ConditionObject();
}
// Methods relayed from outer class
// 获取当前占用线程
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
// 判断当前线程占用了多少资源
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
// 判断是否已加锁
final boolean isLocked() {
return getState() != 0;
}
/**
* Reconstitutes the instance from a stream (that is, deserializes it).
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0); // reset to unlocked state
}
}
然后我们定义一个公平锁类和一个非公平锁类,继承这个Sync
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
这样,公平锁和非公平锁就实现了,他们俩唯一的区别就是公平锁在获取资源的时候,会调用hasQueuedPredecessors()方法判断一下是否存在等待队列,如果有就去乖乖排队,这样就保证了公平性,先来后到。
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
我们用构造方法区控制使用者想要什么类型的锁,至此,公平和非公平我们已经实现。
实现操作部分线程
我们已经有了思路,定义一个个队列来分别存放线程,那么我们可以把这一个个线程定义为对象的一个属性,所以我们定义一个接口,来操作这些线程。这个类就是Condition
public interface Condition {
void await() throws InterruptedException;
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
void signal();
void signalAll();
同样,定义一个类来保存这个队列。
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;
}
public Condition newCondition() {
return sync.newCondition();
}
final ConditionObject newCondition() {
return new ConditionObject();
}
我们只需要调用newCondition()方法就可以获得一个保存一类线程的对象。ConditionObject对象中还有很多方法,我想单独写一篇文章来说明。
至此,操作部分线程的功能我们也实现了,这其实就是ReentrantLock的Condition场景的应用。
实现可中断
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public final void acquireInterruptibly(int arg)
throws InterruptedException {
// 获取锁之前校验一下线程的中断状态
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
// 自旋里这个if判断至少会走两边,每走一遍都去判断一下线程的中断状态
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
中断操作已经在代码里注释说明。
获取锁设定超时时间
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
代码很简单,就是获取锁和每次自旋cas过程前,判断一下是否超时,如果超时,直接返回false
思考
至此,ReentrantLock的大部分功能我们已经实现了,当你去翻阅源码的时候。你会发现,ReentrantLock里也就这么点东西,剩下的方法无非是开发者在开发时封装了一些公用的方法。大家对比一下我们平时开发的过程,流程是不是非常的相似:
- 确定需求
- 确定需求实现的方式
- 书写代码
- 对代码重构,封装公共方法
由此可见,一个类,我们只要知道了设计这个类的初衷是为了解决什么问题,然后我们再去看源码里作者是如何解决这个问题的,用什么方式去解决这个问题。
再深入一点我们可以研究一下,用这种方式去解决这个问题,但是代码为什么要这么写,这么写有什么好处?如果是我们来实现这个功能,我们的代码跟作者的代码孰优孰劣?一番比较下来,我们可以学习源码里对方法的优雅封装,对变量的合理使用以及一些对异常情况的处理方式,源码里真的是行行都是精髓。
写后感
自从开始写博客到现在,我发现我对于一件事情的梳理更快速和规范了,我觉得这有利于我思考,这得益于我每写一篇博客做的总结,这也是一种我认为的提升自己的方式。
比如这篇文章所讲的ReentrantLock和synchronized,在写这篇文章之前,如果你问我他们两个有什么区别,我的回答就是不知道,因为我不能条理清晰的去告诉你这两个的区别和优劣。也就是说原理我懂一点,但是我不知道如何组织语言。
现在如果你们再问我,那么我可以很自信的跟你说个一二三来。这篇文章中有我以前就懂的知识,也有我在写的时候一知半解而去学习的知识。我把这些知识用我的语言梳理脉络去讲给你们听,我自身也在一个高度学习的状态。赠人玫瑰,手有余香。如果大家到了一个学习的瓶颈期,不妨也试试写博客这种方式。