这次介绍一下以AQS为模板的一个简单的锁的实现:ReentrantLock——可重入锁
首先解释两个字:重入。重入就是重新进去,进去哪里?进去被ReentrantLock锁住的代码块。被为什么说这个锁时可重入的,是因为这个锁有一个性质,就是在这个锁已经被当前线程获取的情况下,这个线程再次尝试获取时就不用重新设置锁状态。我们知道,设置锁状态是一个CAS(compare and set)操作,这个操作是性能比较低下的。对于不可重入的锁,如果在一个循环中写了个lock,那么每次循环都去执行这个设置锁状态的CAS操作,性能低下。可重入锁就是解决了这问题。
我说的肯定是看不太懂,还是看代码,从我们使用的lock方法来看:
public void lock() {
sync.lock();
}
可以看到,他是把这个获取锁状态的动作委托给一个叫sync的类来实现的。
这个sync是什么类呢?他是一个继承了AbstractQueuedSyncronizer的内部类,也就是说,他有了最基本的同步功能,会使用队列来管理等待锁状态的线程。
static abstract class Sync extends AbstractQueuedSynchronizer
他也有几个方法,不过抽象类,方法总是给子类使用的,Sync这个类有两个子类:NonfairSync 和 FairSync,分别是非公平的锁和公平的锁。
解释一下公平和非公平:
现在我们知道ReentrantLock的锁功能委托给了其内部类Sync,而Sync又继承了队列同步器AQS,他的子类要获取锁,必然是通过此种方式
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
当然这个tryAcquire(int arg)方法是需要Sync的子类来实现的。我们先来看默认的非公平锁:
public ReentrantLock() {
sync = new NonfairSync();
}
他的lock()方法:
/**
* 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);
}
也是增加了一重检查,先看锁状态state,以期望值0,目标值1对AQS的state进行CAS实施CAS操作,如果失败(期望值不为0),则尝试以AQS模板类的方法尝试获取锁,上面已经讲到了,现在再写一遍,因为很重要:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这个方法描述一个过程,本身不体现公平与非公平,是否公平需要由子类自己实现的tryAcquire(int arg)来体现,我们来看看非公平锁的tryAcquire(int arg)的实现
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
/**
* 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();
//获取AQS中的队列的同步状态
int c = getState();
//如果没有线程占有锁,就获取锁
if (c == 0) {
//以CAS操作设置锁状态
if (compareAndSetState(0, acquires)) {
//设置当前线程为独占线程
setExclusiveOwnerThread(current);
return true;
}
}
//如果尝试获取锁的线程是独占了这个锁的线程
else if (current == getExclusiveOwnerThread()) {
//让他重入这个锁
int nextc = c + acquires;
//如果这个锁在等待什么,也就是c = -2 的情况下,就不能获取锁了
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//获取锁成功了之后把锁状态更新(CAS操作)
setState(nextc);
return true;
}
return false;
}
到现在我们还是不知道上面这段代码哪里体现了不公平,不急我们来看看公平锁的实现,对比一下就晓得了:
/**
* 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 (isFirst(current) &&
//CAS修改锁状态
compareAndSetState(0, acquires)) {
//将当前线程设置为独占线程
setExclusiveOwnerThread(current);
return true;
}
}
//如果当前线程就是独占线程
else if (current == getExclusiveOwnerThread()) {
//独占次数+1
int nextc = c + acquires;
//如果此时线程在等待某condition,此时线程的c = -2(CONDITION状态)
if (nextc < 0)
//抛出无法再有更多的线程获取该锁
throw new Error("Maximum lock count exceeded");
//将锁状态设置为独占次数
setState(nextc);
return true;
}
return false;
}
我们可以看到对比非公平锁,公平锁的 tryAcquire 只是增加了一个 isFirst(current) 的判断,即,公平所要求线程按照CLH队列的原则来依次执行。
举个例子,有一个锁,锁住了若干线程,而且,持有这个锁的线程已经执行完毕,并对后续的线程进行唤醒(unparking),但是后续的线程去执行其他代码了,或者再等待其他condition,并没有在自旋地等待锁的释放,所以即便这个锁就处于没人用的状态,这对性能是极大地浪费。因此非公平锁就取消了这个判断,使得线程在锁可用的情况下,不用管队列的先进先出原则直接获取锁。
实践证明,非公平锁在大部分情况下的性能要优于公平锁。
当然,虽然 tryAcquire 这个方法有公平与非公平之分,但是在其继承的AQS的自旋地等待锁的 acquireQueued(final Node node, int arg) 方法中仍然是公平的,这个不知道为什么这样设计。