What is ReentrantLock
ReentrantLock,从单词字面上理解,就是可重入锁,他内部实现了两种锁的机制,公平锁与非公平锁,排他性的, 继承自AbatractQueuedSynchronizer,依靠着AQS里面的FIFO队列进行线程的调度。可以参看:Java并发学习(三)-AbstractQueuedSynchronizer
Lock介绍
ReentrantLock继承Lock接口,Lock接口定义一系列关于“锁”的规范方法,其代码如下:
public interface Lock {
//阻塞性获取锁
void lock();
//尝试一次获取锁
boolean tryLock();
//给予一定超时time去阻塞性获取锁
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//解锁
void unlock();
//新建返回一个等待队列
Condition newCondition();
}
上述Lock只是一个简单的接口,在concurrent包下面,任何关于“锁”的规则,都是继承自Lock。
下面结合代码具体分析一下ReentrantLock实现原理。
ReentrantLock代码结构
ReentrantLock,可重入锁,它可以等同于synchronized的使用,但是ReentrantLock提供了比synchronized更强大、灵活的锁机制,可以减少死锁发生的概率。
首先看ReentrantLock的定义头:
/**
* 可重入锁,当相同线程再次进入某一个lock块时,不需要再次获取锁的
* 排他性质的,没有读写区别。
*
* 推荐用以下方式写:
* lock.lock(); // block until condition holds
* try {
* // ... method body
* } finally {
* lock.unlock()
* }
*
* @since 1.5
* @author Doug Lea
*/
public class ReentrantLockAnalysis implements Lock, java.io.Serializable
ReentrantLock里面主要包括的变量及方法如下:
- abstract static class Sync extends AbstractQueuedSynchronizer:同步器Sync,相当于对AQS里面方法的封装。
- static final class NonfairSync extends Sync:非公平锁,继承自Sync
- static final class FairSync extends Sync:公平锁
- public void lock():加锁方法
- public void lockInterruptibly() throws InterruptedException:抛出异常的加锁。
- public boolean tryLock():尝试一次获取锁
- public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException
- public void unlock():释放锁
- public int getHoldCount():返回当前代码被锁住的次数
- public boolean isHeldByCurrentThread():返回是否为当前线程获取了锁
- …
ReentrantLock实现原理
简单说说ReentrantLock的结构,首先里面有一个Sync的静态内部类,继承自AQS,因为AQS里面有两种锁机制,排它锁和共享锁,继承的原因是选择了其中一种特性的锁进行具体实现。
其次,在Sync的基础上,衍生处两个子类,NonfairSync(非公平锁)和FairSync(公平锁)。
最后对于ReentrantLock具体锁的实现,默认是通过NofairSync来实现,当然也可以指定公平锁还是非公平锁。然后里面具体实现方法基本都是对AQS的一层封装,所以可以去看我的讲AQS的文章,来加深对ReentrantLock的理解。
接下来详细介绍截个具体的方法加深理解:
Sync
在Sync里面实现了非公平锁的获取:
nonfairTryAcquire方法:
/**
* 非公平的尝试获取锁。
*/
final boolean nonfairTryAcquire(int acquires) {
//捕获当前线程
final Thread current = Thread.currentThread();
//获取state值
int c = getState();
if (c == 0) {
//没线程在lock块,所以去尝试更改state。
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;
}
//获取失败,因为是try,所以只尝试获取一次。
return false;
}
tryRelease方法:
protected final boolean tryRelease(int releases) {
//也只尝试一次,去释放锁。
//因为是重入的,所以是getState-releases;
int c = getState() - releases;
//如果不是当前线程拥有锁,就说明没有权限去释放锁。
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
//如果都走了,也就是重入锁都释放了,那么最终返回true。
free = true;
//置空当前拥有锁的线程
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
当然Sync里面还有些其他方法,比较简单,就不多解释,这里只给出注释:
- protected final boolean isHeldExclusively():判断是否为当前线程
- final ConditionObject newCondition():返回一个Condition
- final Thread getOwner():获取锁的拥有者
- final int getHoldCount():判断被锁了几次
- final boolean isLocked():判断是否被锁
NonfairSync和FairSync
接下来就说说,公平锁和非公平锁,到底是怎么实现的。
公平和非公平,具体有怎样一种实现思路呢?比如你去排队打饭,如果是公平的排队,那么肯定是尊崇先入先出,谁排前面谁先打嘛,插队就是明显不公平的体现了。
从AQS分析中我们知道,AQS维护这一条FIFO队列,所以这就很清晰明了了,公平锁和非公平锁的区别,就是在于队列中等待获取锁的线程将会以怎样一种方式去获取前一个节点已经释放的锁。
有点晕,看代码:
NonfairSync
里面就两个方法,lock
和tryAcquire
,并且只是tryAcquire
是protected
的,tryAcquire
则是调用的父类Sync的nonfairTryAcquire
方法。
这里就说说lock方法:
NonfairSync的lock方法:
/**
* 先尝试强行获取锁,也就是如果state为0,则变为1,成功就设置自己线程
* 失败就常规设置转化。
*/
final void lock() {
//强行获取锁,类似于插个队试试
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//失败了,那我就老实排队吧
acquire(1);
}
接下来直接对比FairSync里面的lock:
//上锁直接按照AQS顺序上锁。
final void lock() {
acquire(1);
}
现在估计懂了公平锁与非公平锁的差别了吧。
其他代码都类似,就不多讲了,如果感觉对ReentrantLock不是很理解,建议先看看JMM与AQS,这里贴出我的两篇博客供阅读哈:
Java并发学习(二)-JMM
Java并发学习(三)-AbstractQueuedSynchronizer
ReentrantLock与synchronized
在Java中,我们可以使用synchronized来实现对资源的锁定,也可以用ReentrantLock来对代码块进行加锁。
二者主要有以下方面区别:
- ReentrantLock更加轻量级,而synchronized就像一把大锁,ReentrantLock基于AQS,是在语言层面上实现的锁,而synchronized则是可能需要切入到内核态去执行,当然Java6一来,对synchronized的优化已经做了很多,一定方面耗时上和ReentrantLock也不相上下了。
- ReentrantLock更加灵活,比如Lock的Java doc里面一个注释中,举了这样一种情况,如果你打算先获取A的锁,再获取B的锁,然后释放A又获取C的锁,然后释放B的锁再获取D的锁等等,试想,这样一种状况,synchronized能够解决么?
- 和synchronized的wait和signal一样,ReentrantLock也有Condition,但是Condition功能比wait和signal强大,并且不用加锁,能够唤醒指定一个或者多个队列。
参考资料
JDK1.8