本文中涉及的源码,来自JDK8。
ReentrantLock,独占式锁,支持一个线程对资源的重复加锁,再次加锁时不会阻塞自己(synchronized关键字隐式地支持锁重入)。此外,它还支持获取锁时选择公平或非公平模式。
那么,何为公平锁?指在绝对时间上,先申请锁的请求一定先被满足,那么这个锁是公平的;反之,是不公平的。可见,获取公平锁时,按照FIFO原则,在同步队列中等待时间最长的线程最先获取锁。
1 创建ReentrantLock
无参构造器默认使用非公平锁。入参为true时使用公平锁。
2 同步器实现
在ReentrantLock中,内部类Sync继承AbstractQueuedSynchronizer,作为同步组件。它有两个子类:NonfairSync、FairSync,分别是非公平锁、公平锁的实现。
3 加锁
分别来看公平、非公平两种模式:
- 非公平模式时,
lock()时先CAS尝试获取锁,如果成功则退出,不用进入同步队列等待。 - 公平模式时,
lock()时直接走AQS的acquire(),如果tryAcquire(arg)失败,线程将进入同步队列等待。
4 锁重入
4.1 加锁
锁重入的实现在ReentrantLock.Sync#nonfairTryAcquire方法中,关键在于:当同步状态不可获取时,锁需要能够识别当前线程是否是锁持有者。
锁对象如何记录锁持有者呢?AbstractQueuedSynchronizer继承了
AbstractOwnableSynchronizer类,后者的exclusiveOwnerThread属性,能够记录持有独占式锁的线程。
当线程重复获得锁时,对state计数自增即可。
4.2 释放锁
tryRelease(int releases)方法中:
- 当线程对锁重入了n次后,前n-1次释放锁时,state计数自减,返回false,并未真正失去锁;
- 第n次释放锁时,计数等于0,方法返回true,才会唤醒阻塞中的后继线程。
5 公平与非公平
区别在于:
- 公平模式时,申请资源的(多个)新线程到来时,即使同步状态可被获取,如果队列中有等候的线程,当前线程也不能尝试去获取,必须将添加到队尾,排队等候。
- 非公平模式时,(多个)新线程申请资源时,可以尝试争抢资源;成功时不用入队,失败时才被添加到队尾排队。
使用非公平锁时,如果一直有新线程到来,可能导致入队很早的线程,很久不能获取到锁,造成饥饿。但是,它能够减小线程切换次数(新线程有很大概率不用入队阻塞),因而非公平锁有更大的吞吐量。
公平锁必须按照FIFO原则,依次排队来获取锁。因此,公平锁往往没有非公平锁的效率高。但是,它可以减少饥饿发生的概率,等待越久的线程越是优先获取到锁。
6 使用ReentrantLock来避免死锁
常常有这样的场景:一个线程任务需要同时获得多把锁时,才能顺利执行。
使用synchronized时,很容易造成死锁。如下代码,t1线程在获得lock1后,阻塞在获取lock2;正好有其他线程获取了lock2,阻塞在获取lock1。此时,发生了死锁。
Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread(() -> {
synchronized (lock1) {
log.debug("lock1");
sleep(1);
synchronized (lock2) {
log.debug("lock2");
// do something
}
}
}, "t1");
如果线程获取更多锁失败时,能够自动释放已经获得的锁,将避免可能发生的死锁问题。
ReentrantLock的tryLock()方法是非阻塞的,使用它很容易实现这个功能。
ReentrantLock lock1 = new ReentrantLock();
ReentrantLock lock2 = new ReentrantLock();
Thread t1 = new Thread(() -> {
while (true) {
if (lock1.tryLock()) {
log.info("t1 get lock1");
try {
// tryLock是非阻塞的
if (lock2.tryLock()) {
log.info("t1 get lock2");
try {
log.info("t1 do something...");
break;
} catch (Exception e) {
} finally {
lock2.unlock();
}
}
} catch (Exception e) {
} finally {
lock1.unlock();
}
}
}
}, "t1");