ReentrantLock
是基于AQS实现的可重入独占锁,如果还不了解AQS实现原理的同学可以先去AQS原理分析学习一哈。
如果文章中由任何不妥或者谬误之处,请批评指正。
公平锁or非公平锁
ReentrantLock
核心功能的实现,依赖于继承AQS类实现的同步器。
ReentrantLock
有三个核心内部类,基于AQS实现的Sync
(同步器类),基于Sync
类实现的NonfairSync
(非公平同步器)和FairSync
(公平同步器)。
公平锁调用了FairSync
类的方法,公平锁的含义是,多个线程依照依次尝试去获取锁,而锁释放以后,等待线程按照先后顺序获取锁。
非公平锁的实现就是调用了NonfairSync
类的方法,非公平锁的含义就是,多个线程有时间顺序的去尝试获取锁,如果获取失败进入等待状态,如果共享资源得到释放,这些等待线程再去竞争获取锁(和尝试获取锁的先后顺序无关),所以是非公平的。
举个通俗的例子就是,一群人去吃烤鸭,店主每次只能烤出一只烤鸭给客户吃,公平锁就是等着吃烤鸭的那些人,排起了长队,而非公平锁就是,等着的那群人不仅不排队,还要互相推搡,争抢着去吃烤鸭。
1. 非公平锁
先讲非公平锁的原因是非公平锁比较常用。ReentrantLock
无参的构造方法就会生成一个非公平锁。
public ReentrantLock() {
// 默认生成的是非公平同步器
sync = new NonfairSync();
}
1.1 非公平锁的获取
我们来直接看一下NonfairSync
的源码。
static final class NonfairSync extends Sync {
// 序列化ID
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
// 尝试快速获取锁
// 如果当前State状态码是0,说明没有线程持有锁
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // AQS中的方法
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
我们重点来看快速获取锁失败以后的操作。调用了acquire(int)
方法,而根据AQS的源码,我们已经了解到,acquire(int)
方法会调用 tryAcquire(int)
方法,饶了一圈又回来了,眼皮子底下的tryAcquire(int)
其实是个皮包公司。
最终还是nonfairTryAcquire(int)
方法真实实现了非公平锁。
而nonfairTryAcquire(int)
又在哪呢?在NonfairSync
父类里!这里不得不吐槽一句了,这种写法十分不符合OO设计原则,估计是Doug Lea大神一开始压根就没想公平锁这个东西,所以就顺手写下来了,至今还没有人敢动过这些代码,所以可见Doug Lea大神不是吾等小辈可以评价的。
我们来看一下nonfairTryAcquire(int)
是怎么设计的。
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread(); // 获取当前线程
int c = getState(); // 获取AQS中的state状态码
if (c == 0) { // state为0则说明没有线程持有锁
if (compareAndSetState(0, acquires)) {
// CAS成功就直接将当前线程设置为共享资源的独占线程
setExclusiveOwnerThread(current);
return true;
}
}
// 下面是重入锁的情况
else if (current == getExclusiveOwnerThread()) {
// 重入锁就让state加上申请次数(这里都是1)
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc); // 重入一次,state就加一
return true;
}
// 如果不能获取锁就返回false
return false;
}
这段代码还是很容易理解的,而且逻辑也比较清晰,可以看到重入锁的设计就是使用了AQS中的state
状态码呀。
// AQS中的同步状态码就是这样婶儿滴
private volatile int state;
ReentrantLock
核心设计思路还是CAS+volatile的机制,确保了线程安全,而通过只使用state
状态码来标识线程状态,比起使用操作系统的锁机制需要上下文的切换,可以说是非常的节省资源了。(顺便一提,目前为止安卓虚拟机上ReentrantLock
的效率比使用synchronized
关键字的效率要高的多)。
如果锁获取失败,那么就进入了我们熟悉的AQS流程。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
无论是addWaiter(Node.EXCLUSIVE)
将当前线程包装成一个队列节点,然后入队,还是 acquireQueued(Node,arg)
方法让线程等待,并且等待唤醒以后竞争资源,这都是AQS为我们实现的方法,在此不做详细解释。
2. 公平锁
公平锁比起非公平锁效率低一些,因为非公平锁有一些快速获取锁的机制,但是这种机制会导致插队的情况发生,而如果没有这些机制,就会需要等待队列中的线程的被唤醒,这个过程由操作系统来安排,所以速度会慢一些。
2.1 公平锁的获取
我们直接来看FairSync
类的实现。
static final class FairSync extends Sync {
// 序列化ID
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
// 没有了快速获取锁的if语句
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); // 获取AQS同步状态码
if (c == 0) { // 说明锁处于释放状态
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
// 如果这个线程在队列中没有前驱节点(是队头或者根本没有建立队列)
// 那么就尝试CAS操作修改同步状态码,并设置线程独占
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;
}
}
FairSync
类中的lock()
方法中没有快速获得锁的if语句,而是直接进入acquire(int)
方法,这是第一点防止插队机制,原理是防止有解锁时,有未在队列中的新建线程,直接获得了锁。
而FairSync
类中的tryAcquire(int)
和NonfairSync
类中的方法只有一处区别,就是判断当前线程在队列中是否有前驱节点,如果没有前驱节点,才能获取锁。这是第二点防止插队机制,原理是防止进入acquire(int)
的线程,还没有入队,又反过头来直接获取了锁。
这两点不同于非公平锁的机制保证了,多个线程会顺序的进入等待队列,最终做到了公平锁的机制。
3. 解锁过程
这个过程相对于获取锁的过程就简单了许多。
public void unlock() {
// 直接调用同步器中的release(int)方法,无论是否为公平锁
sync.release(1);
}
而release(int)
是AQS中的释放资源的方法,也很简单。
public final boolean release(int arg) {
if (tryRelease(arg)) { // 根据子类实现的方法判断
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 唤醒节点内的线程
return true;
}
return false;
}
最终还是ReentrantLock
中的tryRelease
方法在搞事情,我们观察一下它。
protected final boolean tryRelease(int releases) {
int c = getState() - releases; // 调用一次state就减少1
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { // 当state为0时,说明可以释放锁了
free = true;
setExclusiveOwnerThread(null); // 设置资源线程占用为null
}
setState(c); // 修改state状态码
return free;
}
这个方法可以说十分的容易理解了。而state的增加和减少,实现了对于同一线程的锁重入,获取成功一次state就加1,释放锁成功一次就减1,为0时则说明锁已被彻底释放