1.定义
重入锁:能够支持一个线程对资源的重复加锁,也就是当一个线程获取到锁后,再次获取该锁时而不会被阻塞。
2.可重入锁的应用场景
2.1 如果已经加锁,则不再重复加锁,比如:交互界面点击后响应时间长,可能会多次点击,使用重入锁可防止后台重复执行。
if (lock.tryLock()) { //如果已经被lock,则立即返回false不会等待,达到忽略操作的效果
try {
//操作
} finally {
lock.unlock();
}
}
2.2 如果发现该操作已经在执行,则等待一段时间,超时则不再执行。
try {
if (lock.tryLock(5, TimeUnit.SECONDS)) { //如果已经被lock,尝试等待5s,看是否可以获得锁,如果5s后仍然无法获得锁则返回false继续执行
try {
//操作
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace(); //当前线程被中断时(interrupt),会抛InterruptedException
}
2.3 如果操作已经加锁,则等待一个一个加锁。可以防止资源使用冲突,保证同一个时间内只有一个操作可以使用该资源。
lock.lock(); //如果被其它资源锁定,会在此等待锁释放,也将处于阻塞状态
2.4 可中断锁:可响应中断操作,然后释放锁。
lock.lockInterruptibly();
3.原理分析
可重入锁在获取锁时,有公平和非公平获取锁两种方式,默认是后者。如果在绝对时间上,先对锁进行获取的请求先被满足,那么这个锁则是公平锁,也就是等待时间最长的线程优先获取锁。反之,则是非公平锁,非公平锁的效率较高。我们知道ReentrantLock
是通过使用自定义同步器来实现锁的获取和释放的,那么就看一下获取同步状态的代码。
3.1 非公平锁获取状态的代码
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;
}
以上代码逻辑是这样的:当一个线程尝试获取锁时,先判断同步状态值是否为0,如果是,则该现场获取到锁,并返回true;如果同步状态值不为0,则判断该线程是否就是占用该锁的当前线程,如果是,则同步状态值增加,表示同一个线程多次加锁,并返回ture。否则,则获取同步状态值失败,返回false。
3.2 释放状态时的代码
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;
}
在释放锁时,每释放一次,同步状态值就是减少,最终同步状态值为0时,表示完全释放锁,此时将占有线程设置为null,同时返回true。
3.3 公平锁获取状态时的代码
/**
* 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;
}
}
这和nonfairTryAcquire方法唯一的区别就是增加了hasQueuedPredecessors()方法,该方法的作用就是判断同步队列中当前节点是否有前驱节点。如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能
继续获取锁。
现在有个问题需要思考,为什么非公平锁效率更高?
在nonfairTryAcquire(int acquires)方法中,当一个线程请求锁时,只要获取了同步状态即成功获取锁。在这个前提下,刚释放锁的线程再次获取同步状态的几率会非常大,使得其他线程只能在同步队列中等待。它虽然造成了线程饥饿,但是确实极少的线程切换,
保证了更达的吞吐量。而公平性锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换。