重入锁(ReentrantLock)
就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择。
ReentrantLock虽然没能像synchronized关键字一样支持隐式的重进入,但是在调用lock()方
法时,已经获取到锁的线程,能够再次调用 lock()
方法获取锁而不被阻塞。
下面将着重分析ReentrantLock是如何实现重进入和公平性获取锁的特性!
1. 实现重新进入
该特性的实现需要解决以下两个问题:
- 线程再次获取锁:锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。
- 锁的最终释放:线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。
非公平性(默认的)实现为例,源代码如下:
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;
}
成功获取锁的线程再次获取锁,只是增加了同步状态值,这也就要求ReentrantLock在释放同步状态时减少同步状态值,tryRelease(int releases)
源码如下:
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
2. 公平与非公平获取锁的区别
如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。
上面的代码介绍了非公平锁的tryAcquire(int acquires)
,下面介绍一下公平锁的该方法。
下面献上源码:
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;
}
该方法与nonfairTryAcquire(int acquires)
比较,唯一不同的位置为判断条件多hasQueuedPredecessors()
方法,如果该方法返回为true,则表示有线程更早的获取了锁,所以无法获取同步状态。
下面附上一个例子来说明这两种锁:
public class FairAndUnfairTest {
private static Lock fairLock = new ReentrantLock2(true);
private static Lock unfairLock = new ReentrantLock2(false);
public static void main(String[] args) {
FairAndUnfairTest test = new FairAndUnfairTest();
test.fair();
}
public void fair() {
testLock(fairLock);
}
public void unfair() {
testLock(unfairLock);
}
private void testLock(Lock lock) {
// 启动5个Job(略)
for (int i = 0; i < 5; i++) {
new Job(lock).start();
}
}
private static class Job extends Thread {
private Lock lock;
public Job(Lock lock) {
this.lock = lock;
}
public void run() {
// 连续2次打印当前的Thread和等待队列中的Thread(略)
for (int i = 0; i < 2; i++) {
lock.lock();
System.out.println(Thread.currentThread().getName()+ " waiting: " + ((ReentrantLock2)lock).getQueuedThreads());
lock.unlock();
}
}
}
private static class ReentrantLock2 extends ReentrantLock {
public ReentrantLock2(boolean fair) {
super(fair);
}
public Collection<Thread> getQueuedThreads() {
List<Thread> arrayList = new ArrayList<Thread>(super.
getQueuedThreads());
Collections.reverse(arrayList);
return arrayList;
}
}
}
输出:
Thread-0 waiting: []
Thread-2 waiting: [Thread[Thread-1,5,main], Thread[Thread-3,5,main], Thread[Thread-4,5,main], Thread[Thread-0,5,main]]
Thread-1 waiting: [Thread[Thread-3,5,main], Thread[Thread-4,5,main], Thread[Thread-0,5,main], Thread[Thread-2,5,main]]
Thread-3 waiting: [Thread[Thread-4,5,main], Thread[Thread-0,5,main], Thread[Thread-2,5,main], Thread[Thread-1,5,main]]
Thread-4 waiting: [Thread[Thread-0,5,main], Thread[Thread-2,5,main], Thread[Thread-1,5,main], Thread[Thread-3,5,main]]
Thread-0 waiting: [Thread[Thread-2,5,main], Thread[Thread-1,5,main], Thread[Thread-3,5,main], Thread[Thread-4,5,main]]
Thread-2 waiting: [Thread[Thread-1,5,main], Thread[Thread-3,5,main], Thread[Thread-4,5,main]]
Thread-1 waiting: [Thread[Thread-3,5,main], Thread[Thread-4,5,main]]
Thread-3 waiting: [Thread[Thread-4,5,main]]
Thread-4 waiting: []
Process finished with exit code 0
上面只run了公平锁的程序,可以看到下一个执行的线程与同步队列中的线程顺序有关。
非公平性锁出现了一个线程连续获取锁的情况(此处需要读者修改上述代码进行验证!)
非公平性锁可能使线程“饥饿”,为什么它又被设定成默认的实现呢?
我们通过下面的表来分析一下!
在测试中公平性锁与非公平性锁相比,总耗时是其94.3倍,总切换次数是其133倍。可以看出,公平性锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换。非公平性锁虽然可能造成线程“饥饿”,但极少的线程切换,保证了其更大的吞吐量。