前篇写了JUC的基础AQS,其中介绍了它提供的很多模板方法,但是在实际编程中我们不会直接使用它,而是会使用它的各种实现。本文将介绍在实际使用中出现频率很高的一种并发锁——ReentrantLock。
从名字上来看,ReentrantLock具有两个特性,一个是可重入,一个是锁。
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
...
}
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;
}
}
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
public void lock() {
sync.lock();
}
public void unlock() {
sync.release(1);
}
}
ReentrantLock内容定义了一个抽象类Sync,继承自AQS,而不是自己去继承AQS,所有对ReentrantLock的操作都会转化为对Sync的操作。同时又定义了Sync的两个子类FairSync和NonfairSync,分别用于实现公平锁和非公平锁。除非你在生成ReentrantLock时显示的指明需要公平锁,否则默认采用非公平锁。
可重入
先来看下可重入如何实现,以默认的非公平锁举例。可重入,意味着线程在获取锁之后,还可以再次获取锁,同样,获取了多少次,就要释放多少次,否则资源释放不对,别的线程将永远无法获得锁。
final void lock() {
//1、CAS将state置为1
if (compareAndSetState(0, 1))
//2、设置自己为独占线程
setExclusiveOwnerThread(Thread.currentThread());
else
//3、否则尝试获取资源
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//1、如果state为0,说明当前锁没有被占用
if (c == 0) {
//2、CAS尝试将state设为1
if (compareAndSetState(0, acquires)) {
//3、获取锁成功,设置自己为独占线程
setExclusiveOwnerThread(current);
return true;
}
}
//4、如果持有当前锁的线程就是自己
else if (current == getExclusiveOwnerThread()) {
//5、那么将state增加acquires
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//6、更新state,因为锁已经被自己持有了,所以这里不需要CAS设置
setState(nextc);
return true;
}
return false;
}
//AQS
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
获取锁的过程中,4、5、6三步即实现了可重入获取。再看下释放。
protected final boolean tryRelease(int releases) {
//1、每次释放,将state减去释放数
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//2、如果state为0,说明所有资源都已经释放
if (c == 0) {
free = true;
//3、将独占线程置空
setExclusiveOwnerThread(null);
}
//4、更新state
setState(c);
return free;
}
同样,释放时需要将获取的资源依次扣除,什么时候state减为0了,才算该线程持有的所有资源都释放掉了。
公平锁实现可重入同理,不再赘述。
公平与非公平
那么公平锁与非公平锁又有什么区别呢?
非公平锁的获取上面已经分析了,来看下公平锁的获取,看下有什么不同。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//1、等待队列中是否已经有节点在等待获取锁了
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;
}
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
//2、等待队列以后已经初始化,并且有别的线程正在入队(enq)或者已经入队
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
公平和非公平的区别就在于,尝试获取锁前要看下是否有线程已经在自己之前就开始等待了,如果没有才去竞争。通过这种方式保证公平,即先等先得。
公平锁和非公平锁哪种性能更好呢,公平锁虽然能保证等待最久的线程可以先获得锁,但是这同时也以为着每次都会是不同的线程获取锁,每次都要进行线程切换。《Java并发编程的艺术》进行了测试,表明非公平锁虽然可能造成某些线程一直获取不到锁,但是可以提高整体的吞吐量,所以ReentrantLock将其作为了默认实现。如果是需要保证这种先等先得的特性,也可以使用公平锁。
与synchronized对比
ReentrantLock在加锁和内存语义上提供了与synchronized相同的功能,此外还提供了定时、可中断、公平性等特性。JDK5时,ReentrantLock的性能要远优于synchronized,但是JDK6引入了synchronized的锁优化,两者之间的差别并没有那么大了。
《Java并发编程实战》作者建议,除非需要一些高级功能,如可定时、可轮询、可中断、公平性等,才使用ReentrantLock,否则应该优先使用synchronized,毕竟大部分人都用习惯了,而且使用简单。