1. 前言
昨天鸽了一天,今天我们继续从源码层面来学习ReentrantLock这个可重入锁。
ReentrantLock是一种独占式的可重入锁,位于java.util.concurrent.locks中,是Lock接口的默认实现类,底部的同步特性基于AQS实现,和synchronized关键字类似,但更灵活、功能更强大、也是目前实战中使用频率非常高的同步类。
2. 锁的分类
在学习ReentrantLock之前,我们来认识下Java中几种不同锁的定义,方便我们后续理解文章中涉及到的锁。
2.1 乐观锁和悲观锁
- 乐观锁:乐观锁就是每次拿数据时都假设别人不会修改数据,不会上锁,在更新数据的时候会进行判断数据有没有被修改,如果被修改,就会自动重试
- 悲观锁:悲观锁就是每次拿数据都会认为会有人修改数据,每次操作都会上锁,堵塞其他线程
2.2. 独占锁与共享锁
- 独占锁:同一时间段,一把锁只能被一个线程获取,比如synchronized关键字就是独占锁。
- 共享锁:同一时间段,一把锁可以被多个线程获取,比如Semaphore(信号量),CountDownLatch(倒计时器)都是共享锁
2.3. 公平锁与非公平锁
- 公平锁:按照申请锁的时间先后来获取锁,这种锁往往性能稍差,因为要保证申请时间上的顺序性;
- 非公平锁:后续新来获取锁的线程会先插队,先尝试获取锁,没有获取到锁再加入队列中阻塞等待。
2.4. 可重入锁和不可重入锁
所谓可重入锁就是一个线程在获取到了一个对象锁后,线程内部再次获取该锁,依旧可以获得,即便持有的锁还没释放,仍然可以获得,不可重入锁这种情况下会发生死锁!
可重入锁在使用时需要注意的是:由于锁会被获取 n 次,那么只有锁在被释放同样的 n 次之后,该锁才算是完全释放成功,接下来在文章我们也会讲解到这点
2.5. 可中断锁与不可中断锁
- 可中断锁:在获取锁的过程中可以中断获取,不需要非得等到获取锁后再去执行其他逻辑;
- 不可中断锁:一旦线程申请了锁,就必须等待获取锁后方能执行其他的逻辑处理。
3. 源码解读
3.1. Sync
ReentrantLock 在内部通过构造器来实现公平锁与非公平锁的设置,默认为非公平锁,同样可以通过传参设置为公平锁。底层实现其实是通过FairSync、NonfariSync这个两个内部类,源码如下:
//无参构造,默认为非公平锁 public ReentrantLock() { sync = new NonfairSync(); } // 传入一个 boolean 值,true 时为公平锁,false 时为非公平锁 public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
这两个内部类都继承了内部的Sync同步类,Sync内部类继承了AQS,源码如下,可根据注解阅读,前面几篇文章各种同步工具类已经讲过了Sync的实现,其实原理都差不多,这里就不细讲了:
abstract static class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = -5179523762034025860L; // 定义获取锁抽象方法,让公平锁和非公平锁两个子类实现 abstract void lock(); // 本身并没有提供获取非公平独占锁的默认实现,我们这里需要自己编写 final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); // 判断state是否为0 if (c == 0) { // 尝试使用CAS操作获取锁 if (compareAndSetState(0, acquires)) { // 设置当前线程为锁占用线程 setExclusiveOwnerThread(current); return true; } } // 如果之前已经获取过锁,让state+1即可,实现可重入性质 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; } // 实现AQS的钩子函数,定义释放锁的逻辑 protected final boolean tryRelease(int releases) { // state - 1 int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; // state降到0,代表当前线程真正释放了锁 if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; } }
3.2. FairSync 公平模式
我们接着来看看FairSync和NonfairSync的源码,他们的区别主要在于第一次获取锁时是否会插队
static final class FairSync extends Sync { protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); // 如果state=0,进入后续判断 if (c == 0) { // 判断在等待队列中,当前线程的前面是否存在线程,如果存在那么我们就不需要获取锁, // 按顺序来,实现公平获取锁 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } // 如果当前线程已经占有锁,让state+1即可,不用重新获取 else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } }
这里在通过CAS操作修改state时,也就是获取锁时,首先调用!hasQueuedPredecessors(),来判断当前线程前面是否有正在排队的线程,也就是按顺序来,如果有,就不会获取到锁,从而实现了公平获取锁的性质。
hasQueuedPredecessors():
如果当前线程之前有一个排队的线程,则为 true,如果当前线程位于队列的顶部或队列为空,则为 false。
其后通过current == getExclusiveOwnerThread()当前线程是否是判断持有锁的线程,如果是,就直接让state+1,不需要再次获取锁,实现锁的可重入性。
3.3. FairSync 非公平模式
FairSync 非公平模式的实现特别简单,源码如下
static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; /** * Performs lock. Try immediate barge, backing up to normal * acquire on failure. */ final void lock() { // 直接尝试获取锁 if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else // 没获取到锁,再通过acquire方法阻塞获取锁 acquire(1); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } }
调用lock方法会直接通过Cas操作compareAndSetState(0, 1)获取锁,如果没获取成功就调用acquire(1)去获取锁,acquire是AQS内实现的方法,他会调用钩子函数tryAcquire(在Sync中实现了)获取锁,如果获取失败就会加入CLH队列等待。这样就实现了非公平的特性,在获取锁时先插队尝试直接获取锁,没有获取到才加入CLH队列中等待。
4. 基本使用
我们接下来通过一个案例代码,来使用一下非公平锁模式下的ReentrantLock的使用
public class Test { //初始化一个静态lock对象 private static final ReentrantLock lock = new ReentrantLock(); //初始化计算量值 private static int count; public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(()->{ for (int i = 0; i <1000 ; i++) { lock.lock(); try { count++; } finally { lock.unlock(); } } }); Thread thread2 = new Thread(()->{ for (int i = 0; i < 1000; i++) { lock.lock(); try { count++; } finally { lock.unlock(); } } }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println("result:"+count); } }
输出结果为:2000,thread1和thread2分别做了加1000次的操作,由于通过ReentrantLock对修改操作上锁了,故最终结果是正常的。
5. 总结
ReentrantLock是一种独占式的可重入锁,位于JUC包下,是Lock接口的默认实现类。有三个特性:支持可重入,支持公平与非公平特性,提供堵塞锁和非阻塞锁两种获取方法(lock和trylock)
其中内部锁的竞争是基于AQS实现的,当某一线程获取锁后,将state值+1,并记录下当前持有锁的线程,再有线程来获取锁时,判断这个线程与持有锁的线程是否是同一个线程,如果是,将state值再+1,这样就实现了锁的可重入。当线程释放锁时,将state值-1,当state值减为0时,表示当前线程彻底释放了锁,唤醒等待队列中的线程,使其重新竞争锁。
ReentrantLock公平与非公平的特性,主要体现在竞争锁的时候,是否需要判断AQS队列中是否存在等待中的线程。公平锁需要判断,如果有就加入队列,而非公平锁不需要判断,会先尝试获取锁,没有获取到再加入队列等待。