作为关键字synchronized的替代品(或者说是增强版),重入锁是synchronized的功能扩展。在JDK 1.5的早期版本中,重入锁的性能远远好于synchronized,但从JDK 1.6开始,JDK优化了synchronized,使两者性能差距不大。重入锁使用java.util.concurrent.locks.ReentrantLock类来实现。
使用重入锁可以指定何时加锁和何时释放锁,对逻辑控制的灵活性远远好于synchronized,退出临界区时必须释放锁。之所以称为重入锁,是因为一个线程可以连续获得这种锁。而synchronized隐式地支持重进入。具体而言,重进入指线程在获取锁之后能再次获取该锁而不会被锁阻塞,该特性的实现需要解决两个问题:
1 线程再次获取锁
锁需要识别获取锁的线程是否为当前占有锁的线程,如果是,则再次成功获取。
2 锁的最终释放
线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求获取锁需要计数自增,计数表示锁被重复获取的次数,而锁被释放时计数自减,当计数为0时表示锁已经最终成功释放。
ReentrantLock通过组合自定义同步器实现锁的获取与释放,以非公平性(默认)实现为例,获取同步状态代码如下:
/**
* 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;
}
再次获取同步方法的处理逻辑:通过判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则增加同步状态值并返回true,表示获取同步状态成功。也就是说,成功获取锁的线程再次获取锁,只是增加了同步状态值,这也要求ReentrantLock在释放同步状态时减少同步状态值,释放同步状态代码如下:
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;
}
如果锁被获取了n次,那么前n-1次tryRelease(int releases)方法必须返回false。也就是说,只有同步状态完全释放了,该方法才能返回true。该方法将同步状态是否为0作为最终释放的条件,当同步状态为0时,将占有线程设置为null,并返回true,表示释放成功。
简单的重入锁使用例子如下:
修改上述代码第7-12行:
在这种情况下,一个线程连续两次获得同一把锁。如果不允许该操作,那么同一个线程在第2次请求获得锁时,会和自己产生死锁,程序卡死在第2次申请锁的过程中。如果同一个线程多次获得锁,那么释放锁的次数与其相同。如果释放锁的次数多,那么会有java.lang.IllegalMonitorStateException异常;反之,如果释放锁的次数少了,那么该线程依然持有这个锁。
除了使用上的灵活性之外,重入锁还提供了一些高级功能:
1 中断响应
对于synchronized,如果一个线程在等待锁,那么结果只有两种可能:要么获得这把锁继续执行,要么保持等待。而使用重入锁可以提供另外一种可能:线程可以被中断。也就是说,线程在等待锁的过程中,程序可以根据需要取消对锁的请求。例如,小王与小明一起去打球,如果小王等了10分钟,小明还没到,接到小明的电话了解到由于突发情况不能如约了,那么小王就扫兴地打道回府了。对于线程而言,如果一个线程正在等待锁,那么它可以收到一个通知,被告知无需再等待,可以停止工作了。这种情况对于处理死锁是有一定帮助的。
2 锁申请等待限时
除了等待外部通知之外,避免死锁还有另外一种方法:限时等待。继续上面的例子,如果小王等了半个小时,小明还不来,那么小王就扫兴离去了。对于线程而言,如果给定一个等待时间,让线程自动放弃,那么可以避免线程持续等待。
3 公平锁
在大多数情况下,锁的申请都是非公平的。只要CAS设置同步状态成功,则表示当前线程获得了锁。也就是说,线程1首先申请了锁A,接着线程2也申请了锁A,那么当锁A可用时,哪个线程获得锁是不一定的,系统会从锁A的等待队列中随机选择一个,不能保证公平性。如同买票不排队,大家乱哄哄地围在售票窗口前,售票员忙得焦头烂额,顾不得谁先谁后,随便找个人出票就完事了。而公平锁保证锁的获取遵守FIFO原则即先到先得,不会产生饥饿现象,代价是大量的线程切换。synchronized产生的锁是非公平的,而重入锁可以设置是否公平。公平的重入锁要求系统维护一个有序队列,实现成本比较高,性能较低。所以,默认情况下的锁是非公平的。
ReentrantLock类常见方法如下:
1 public ReentrantLock(boolean fair)
当参数fair是true时,表示该重入锁是公平的。
2 lock()
获取锁,如果锁已被占用,则等待。
3 lockInterruptibly()
获取锁,但优先响应中断。
4 tryLock()
尝试获取锁,如果成功,返回true;否则,返回false。该方法不等待,立即返回。
5 tryLock(long time, TimeUnit unit)
在给定时间内尝试获取锁。
6 unlock()
释放锁。
重入锁的实现包含3个要素:
1 原子状态
原子状态使用CAS操作来存储当前锁的状态,判断锁是否已被其他线程持有。
2 等待队列
所有没有获取到锁的线程会进入等待队列进行等待。当有线程释放锁后,系统就从等待队列中唤醒一个线程,继续工作。
3 阻塞原语park()和unpark()
挂起和恢复线程,没有获取到锁的线程会被挂起。
参考资料
《实战Java高并发程序设计》 P71-80
《Java并发编程的艺术》 5.3 重入锁