重入锁(ReentrantLock),就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁还支持获取锁时的公平性和非公平性选择(默认是非公平)。它是一种独占锁。
ReentrantLock常常与synchronized对比分析:
(1)synchronized是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活。ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。
(2)synchronized可重入,因为加锁和解锁自动进行,不必担心最后是否释放锁;ReentrantLock也可重入,但加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁。
(3)synchronized不可响应中断,一个线程获取不到锁就一直等着;ReentrantLock可以响应中断。
如果使用自定义的独占锁,重复调用lock则会导致线程阻塞(进入死锁状态)。而ReentrantLock在调用lock方法时,已经获取锁的线程能够再次调用lock取得锁而不被阻塞。
// 不会阻塞
public static void main(String[] args){
ReentrantLock lock = new ReentrantLock();
lock.lock();
System.out.println("第一次上锁");
lock.lock();
System.out.println("第二次上锁");
lock.unlock();
System.out.println("释放第二个锁");
lock.unlock();
System.out.println("释放第一个锁");
}
// 自定义锁线程阻塞
public static void main(String[] args){
MyLock lock = new MyLock();
lock.lock();
System.out.println("第一次上锁");
lock.lock();
System.out.println("第二次上锁");
lock.unlock();
System.out.println("释放第二个锁");
lock.unlock();
System.out.println("释放第一个锁");
}
重进入是指任意线程在获取锁之后能够再次获取该锁而不会被锁阻塞。
ReentrantLock是通过AQS实现的重进入。
ReentrantLock通过两个内部类来分别实现公平性和非公平性:
// 非公平获取,通过NonfairSync中的tryAcquire方法,其内部其实是调用的Sync的nonfairTryAcquire
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 判断是否是持有锁的线程,如果是,同步状态增加并返回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;
}
// 公平获取
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;
}
}
// 判断是否是持有锁的线程,如果是,同步状态增加并返回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;
}
可以看到,在同步状态获取时,会在方法内部对线程进行判断,看啥是否是持有同步状态的线程,如果是,则同步状态增加,返回true,表示再次获取成功。
ReentrantLock在获取时是增加同步状态,那么在释放时就是减少同步状态
// 公平锁非公平锁都是通过Sync中的tryRelease方法释放
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次之前,锁的free标记都会是false,直到全部释放完即同步状态为0时,才会返回true,表示锁释放成功。
锁获取的公平性与非公平性问题
公平锁的机制没有非公平的效率高,但是公平锁能够减少“饥饿”发生的概率:等待越久的请求越是能够优先满足。
通过上面的源码可以看到,ReentrantLock非公平锁获取的代码逻辑是:只要CAS设置同步状态成功,就表示线程获取锁。
公平锁则与非公平锁不同之处在于:它在判断条件中多了一个hasQueuedPredecessors()方法。该方法是判断当前节点在同步队列中是否有前驱节点,即是否有比它等待时间更长的节点,如果返回true,这说明有更早到的节点,那么当前节点就获取失败。
前面提到,非公平锁会引发线程“饥饿”,那么为什么还会默认实现非公平锁?
原因是公平锁每次获取锁都会进行上下文切换(FIFO形式轮流执行),而非公平锁则不会每次都切换,这就会使得非公平性锁的开销远小于公平锁。