目录
- ReentrantLock 简介
- ReentrantLock 使用示例
- ReentrantLock 与 synchronized 的区别
- ReentrantLock 实现原理
- ReentrantLock 源码解析
ReentrantLock 简介
ReentrantLock 是 JDK 提供的一个可重入的独占锁,
- 独占锁:同一时间只有一个线程可以持有锁
- 可重入:持有锁的线程可重复加锁,意味着第一次成功加锁时,需要记录锁的持有者为当前线程,后续再次加锁时,可以识别当前线程。
ReentrantLock 提供了公平锁以及非公平锁两种模式,要解释这两种模式不异同,得先了解一下 ReentrantLock 的加锁流程,ReentrantLock 基于 AQS 同步器实现加解锁,基本的实现流程为:
线程 A、B、C 同时执行加锁,加锁是通过CAS操作完成,CAS 是原子操作,可以保证同一时间只有一个线程加锁成功,假设线程 A 加锁成功,则线程 B、C 进入 AQS 等待队列并被挂起,假设 B 在前,C 在后,当线程 A 释放锁时,会唤醒排在等待队列队首的线程 B,该线程会尝试通过 CAS 进行获取锁。如果线程 B 尝试加锁的同时,有线程 D 也同时进行加锁,如果线程 D 与 线程 B 竞争加锁,则为非公平锁,线程 D 加入等待队列排在线程 C 之后,则为公平锁。
- 非公平锁:加锁时会与等待队列中的头节点进行竞争。
- 公平锁:加锁时首先判断等待队列中是否有线程在排队,如果没有则参与竞争锁,如果有则排队等待。
所谓公平就是,大家一起到,就竞争上岗,如果已经有人在排队了,那就先来后到。
使用示例
使用伪代码表示
class X { private final ReentrantLock lock = new ReentrantLock(); public void m() { lock.lock(); // block until condition holds try { // ... method body } finally { lock.unlock(); } }}
默认实现的是非公平锁,如果要使用公平锁,只需要创建 ReentrantLock 对象时传递入参 true 即可,使用方法与非公平锁一样。
private final ReentrantLock lock = new ReentrantLock(true);
condition 使用示例:
class X { private final ReentrantLock lock = new ReentrantLock(); private final Condition condition = lock.newCondition(); public void poll() { lock.lock(); // block until condition holds try { while(条件判断表达式) { condition.wait(); } } finally { lock.unlock(); } } public void push() { condition.signal(); }}
ReentrantLock 与 synchronized 的区别
ReentrantLock 提供了 synchronized 类似的功能和内存语义。
相同点:
- ReentrantLock 与 synchronized 都是独占锁,可以让程序正确同步。
- ReentrantLock 与 synchronized 都可重入锁,可以在循环中使用 synchronized 进行加锁并不用担心解锁问题,但 ReentrantLock 则必须要进行与加锁相同次数的解锁操作,不然可能导致没有真正解锁成功。
不同点:
- synchronized 是JDK提供的语法,加锁解锁的过程是隐式的,用户不用手动操作,操作简单,但不够灵活。
- ReentrantLock 需要手动加锁解锁,且解锁次数必须与加锁次数一样,才能保证正确释放锁,操作较为复杂,但是因为是手动操作,所以可以应付复杂的并发场景。
- ReentrantLock 可以实现公平锁
- ReentrantLock 可以响应中断,使用 lockInterruptibly 方法进行加锁,可以在加锁过程中响应中断,synchronized 不能响应中断
- ReentrantLock 可以实现快速失败,使用 tryLock 方法进行加锁,如果不能加锁成功,会立即返回 false,而 synchronized 是阻塞式。
- ReentrantLock 可以结合 Condition 实现条件机制。
可以看到,ReentrantLock 与 synchronized 都是实现线程同步加锁,但 ReentrantLock 比起 synchronized 要灵活很多。
实现原理
ReentrantLock 使用组合的方式,通过继承 AQS 同步器实现线程同步。通过控制 AQS 同步器的同步状态 state 达到加锁解锁的效果,该状态默认为 0,代表锁未被占用,加锁则是通过 cas 操作将其设置为 1,cas 是原子性操作,可以保证同一时间只有一个线程可以加锁成功,同一个线程可以重复加锁,每次加锁同步状态自增 1,释放锁的过程就是将同步状态自减,减到 0 时才算完全释放,这也解释了为什么释放锁的次数必须与加锁次数一样的问题,因为只有次数一样才能将同步状态减至 0,这样其它线程才能进行加锁。
源码分析
Lock 接口
ReentrantLock 实现了 Lock 接口,这是 JDK 提供的所有 JVM 锁的基类。
public interface Lock { // 阻塞式加锁 void lock(); // 阻塞式加锁,但可以响应中断,加锁过程中线程中断,抛出 InterruptedException 异常 void lockInterruptibly() throws InterruptedException; // 快速失败加锁,只尝试一次, boolean tryLock(); // 阻塞式加锁,可以响应中断并且实现超时失败 boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 释放锁 void unlock(); // 实现条件 Condition newCondition();}
通过代码可以看到,ReentrantLock 的内部实现都是通过 Sync 这个类实现,可以认为遵守组合设计原则,Sync 是 ReentrantLock 的内部类。这里的方法调用,并没有区分是公平锁还是非公平锁,而是无差别地调用,所以区别一定在 Sync 这个类的实现中。
public class ReentrantLock implements Lock, java.io.Serializable { private final Sync sync; public void lock() { sync.lock(); } public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); } public boolean tryLock() { return sync.nonfairTryAcquire(1); } public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(timeout)); } public void unlock() { sync.release(1); } public Condition newCondition() { return sync.newCondition(); }}
Sync 类继承了 AQS 同步器,通过同步器实现线程同步,因为是独占锁,所以最重要的就是实现 tryAcquire 与 tryRelease 两个方法,Sync 类是一个 abstract 类,它拥有两个实现类 FairSync 与 NonfairSync,通过名字应该就可分辨他们就是公平锁与非公平锁。
abstract static class Sync extends AbstractQueuedSynchronizer { abstract void lock(); // 非公平加锁 final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { // 如果没有线程执行锁 if (compareAndSetState(0, acquires)) { // 通过 CAS 尝试加锁 setExclusiveOwnerThread(current); // 加锁成功,设置锁的拥有者为当前线程 return true; } } else if (current == getExclusiveOwnerThread()) { // 如果锁已经被当前线程占有,说明是重复加锁 int nextc = c + acquires; // 将同步状态进行自增,acquires 的传值为 1 if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } // 释放锁 protected final boolean tryRelease(int releases) { int c = getState() - releases; // 将同步状态进行自减,acquires 的传值为 1 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { // 当同步状态减成 0 时,代表完全释放锁,将锁的拥有者置空 free = true; setExclusiveOwnerThread(null); } setState(c); return free; } // ...省略其它...}
所以公平锁与非公平锁的玄机就在 ReentrantLock 的构造方法中,默认的无参构造方法创建非公平锁,如果传参 true,则创建公平锁。而这两个锁都是 Sync 的子类,使用了不同的实现策略,可以认为使用了策略模式。
public class ReentrantLock implements Lock, java.io.Serializable { // 默认创建非公平锁 public ReentrantLock() { sync = new NonfairSync(); } // 如果为 true,则创建公平锁 public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }}
接下来分别看一下 FairSync 与 NonfairSync 是如何实现公平锁与非公平锁的,首先分析非公平锁
static final class NonfairSync extends Sync { // 阻塞式加锁 final void lock() { // 首先尝试竞争加锁,如果成功则设置当前线程为锁的拥有者 if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); // 使用 AQS 排队 } // 尝试加锁 protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); }}
阻塞式加锁调用 ReentrantLock 的 lock() 方法,该方法调用 sync.lock() 执行加锁,非公平锁也就是调用 NonfairSync 类的 lock 方法,该方法首先尝试竞争加锁,此时有三种情况:
- 此时锁没有人持有,竞争成功,直接设置当前线程为锁的拥有者并返回
- 此时锁没有人持有,竞争失败,走 AQS 加锁流程
- 此时锁被其它线程拥有,走 AQS 加锁流程
- 此时锁被自己拥有,竞争失败,走 AQS 加锁流程
AQS 加锁流程就是调用 tryAcquire 方法尝试加锁,如果成功则返回加锁成功,如果失败则进入等待队列并挂起,等待锁的持有者释放锁时唤醒等待队列中的线程,并再次尝试加锁,如此反复,直到加锁成功。
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt();}
AQS 加锁流程在 AQS 中已经提供了完整实现模板,不需要去了解底层就可以使用,需要做的就是自行实现 tryAcquire 方法,NonfairSync 的 tryAcquire 方法这里再贴一次实现代码。
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { // 如果没有线程执行锁 if (compareAndSetState(0, acquires)) { // 通过 CAS 尝试加锁 setExclusiveOwnerThread(current); // 加锁成功,设置锁的拥有者为当前线程 return true; } } else if (current == getExclusiveOwnerThread()) { // 如果锁已经被当前线程占有,说明是重复加锁 int nextc = c + acquires; // 将同步状态进行自增,acquires 的传值为 1 if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false;}
接下来看一下 FairSync 的实现
static final class FairSync extends Sync { final void lock() { acquire(1); } 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; // 将同步状态进行自增,acquires 的传值为 1 if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }}
释放锁的过程,公平锁与非公平锁是一样的,前面的代码中已经解释过了,这里就不再多说了。