前言
了解了Java的队列式同步器AQS的基本实现,接下来可以看看Java中频繁使用的可重入锁ReentrantLock。ReentrantLock是基于AQS实现的,内部类Sync继承了AQS,提供了公平锁和非公平锁。同Synchronized相比,ReentrantLock是代码实现的锁,而Synchronized是虚拟机上实现的锁,都是可重入的排他锁,即一个线程占有锁,其他线程必须等待锁的释放,同一个线程多次进入已将占有的锁,在效率方面,之前是ReentrantLock 效率更高,但后来Java对Synchronized进行了优化,效率目前来说差不多,ReentrantLock使用起来比Synchronized更加灵活,Synchronized使用来说更加方便,不用担心加锁释放锁的代码。
公平锁和非公平锁
先来看看公平锁和非公平锁的不同:
- 公平锁:顾名思义,就是完全按照线程请求锁的先后顺序来获得锁,先到先得,如果等待锁的队列中还要其他线程,必须排在其后面等待
- 非公平锁:在线程请求锁的同时,该锁被释放,则该线程会直接获得锁,无需经过等待队列,如果请求锁的同时锁被占有,则和公平锁一致,需要在等待队列中排队等候
公平锁和非公平锁各有各的应用场景,因为一个线程从唤醒到持有锁执行任务之间有着严重的延迟,假设一个线程A释放锁,需要唤醒其后继节点的线程B,B在从唤醒到持有锁有一段时间的延迟中,有一个线程C尝试获取锁,如果采用非公平锁,这时C会获得锁,如果C执行时间很短,在B唤醒前已经执行完毕,则B唤醒则直接获得锁执行,但如果C执行时间很长,在B唤醒前无法执行完毕,则B唤醒后获取锁失败会被重新挂起。
因此对于那些线程持有锁的时间较长的情况,采用公平锁更合适,减少不必要的唤醒(在非公平锁下唤醒得不到锁被重新挂起)带来的消耗,对于线程持有锁时间较短的情况,非公平锁可以提高效率,即不影响等待队列中线程运行,同时能运行完插队的线程。
总的来说,公平锁保证线程请求资源上的绝对公平,但是频繁的切换上下文,非公平锁可以降低上下文切换,降低性能开销,有更大的吞吐量,但是非公平锁有可能造成饥饿现象。
ReentrantLock
ReentrantLock是可重入独占锁,其内部是通过调用内部类Sync的方法实现对锁的控制,Sync是继承了AQS类,其使用了模板方法,子类只需实现tryAcquire和tryRelease方法返回获取锁和释放锁是否成功的boolean即可,如果获取锁线程会被挂起在同步队列中(在AQS中实现)因此ReentrantLock中Sync只需考虑判断是否获取锁成功以及判断是否释放锁,同时可以通过setExclusiveOwnerThread设置线程为独占锁的线程(传入null代表无线程占有锁,一般在释放锁时调用),AQS类的详细解读可以看 Java 同步器 - AQS。
非公平锁的实现
ReentrantLock考虑到系统的性能,默认是采用非公平锁,从其构造函数可以看出
1 | // 默认为非公平锁 |
NonfairSync就是ReentrantLock中非公平锁的实现,其继承了Sync。
1 | abstract static class Sync extends AbstractQueuedSynchronizer { |
可以看到非公平锁lock()过程是,首先通过CAS设置State,如果设置成功,则说明获取锁成功,失败再调用acquire,调用tryAcquire方法,tryAcquire方法使用父类Sync的nonfairTryAcquire,这也是一次尝试通过CAS设置State,如果设置成功,则说明获取锁成功。可以看到如果有线程占有锁,会进行判断当前线程和占有锁的线程是否为同一个线程,如果是,state加1,获取锁成功,这里实现了可重入的功能。而state是判断能否释放锁的依据。
公平锁的实现
1 | static final class FairSync extends Sync { |
其实公平锁和非公平锁的实现几乎类似,公平锁的lock方法直接调用acquire方法,没有非公平锁的抢占的操作,acquire最终会调用tryAcquire尝试获取锁(AQS中的实现),公平锁的tryAcquire方法在获取线程时比非公平锁多了一个hasQueuedPredecessors判断,判断等待队列中是否有前驱节点,如果有则不去尝试获取锁,直接判断获取失败。
释放锁
1 | protected final boolean tryRelease(int releases) { |
释放锁的过程是相同的,由于是可重入锁,state记录线程重入的次数,所以每当线程释放一次锁state减1,直到state的为0时代表线程重入锁已经全部释放,这时候可以释放锁给其他线程。由于tryRelease调用会减少state,所以是不允许未获得锁的线程释放锁的(即没有调用lock方法就调用release方法),否则会造成出错。