学习完 AQS,本文我们就来研究第一个 AQS 的实现类:ReentrantLock。
ReentrantLock
可重入锁,可重入表示同一个线程可以对同一个共享资源重复的加锁或者释放锁。
具备与使用 synchronized 方法和语句访问的隐式监视器锁相同的基本行为和语义的可重入互斥锁,但具备扩展功能。
ReentrantLock
由最后成功锁定但尚未解锁的线程所拥有。当另一个线程不拥有该锁时,调用该锁的线程将成功返回该锁。假如当前线程已经拥有该锁,则该方法将立即返回。可以使用 isHeldByCurrentThread 和getHoldCount 方法进行检查。
此类的构造函数接受一个可选的 fairness 参数。设置为true时,在争用下,锁倾向于授予给等待时间最长的线程。否则,此锁不能保证任何特定的访问顺序。使用多线程访问的公平锁的程序可能会比使用默认设置的程序呈现较低的总吞吐量(即较慢;通常要慢得多),但取得锁并保证没有饥饿的时间差异较小。但是请注意,锁的公平性不能保证线程调度的公平性。因而,使用公平锁的多个线程之一可能会连续屡次取得它,而其余活动线程没有进行且当前未持有该锁。还要注意,未定时的 tryLock 方法不支持公平性设置。假如锁可用,即便其余线程正在等待,它将成功。
建议的做法是始终立即在调用后使用try块进行锁定,最常见的是在构造之前/之后,例如:
class X { private final ReentrantLock lock = new ReentrantLock(); // ... public void m() { lock.lock(); // block until condition holds try { // ... method body } finally { lock.unlock() } } }
除了实现Lock接口之外,此类还定义了许多用于检查锁状态的 public 方法和 protected 方法。 其中少量方法仅对检测和监视有用。
此类的序列化与内置锁的行为相同:反序列化的锁处于解锁状态,而不论序列化时的状态如何。
此锁通过同一线程最多支持2147483647个递归锁。 尝试超过此限制会导致锁定方法引发错误。
ReentrantLock 本身不继承 AQS,而是实现了 Lock 接口
Lock 接口定义了各种加锁,释放锁的方法,比方 lock() 这种不响应中断获取锁,在ReentrantLock 中实现的 lock 方法是通过调用自己设置的同步器 Sync 中的的同名笼统方法,再由两种模式的子类具体实现此笼统方法来获取锁。
ReentrantLock 就负责实现这些接口,使用时,直接调用的也是这些方法,这些方法的底层实现都是交给 Sync 实现。
无参数构造方法
相当于 ReentrantLock(false),默认为非公平的锁
有参构造方法,可以选择锁的公平性
可以看出
结构图
可见是ReentrantLock的笼统静态内部类 Sync 继承了 AbstractQueuedSynchronizer ,所以ReentrantLock依靠 Sync 就持有了锁的框架,只要要 Sync 实现 AQS 规定的非 final 方法就可,只交给子类 NonfairSync 和 FairSync 实现 lock 和 tryAcquire 方法
Sync 对象的非公平锁
非公平模式的 lock 方法
这里的 lock 方法并没有直接调用 AQS 提供的 acquire 方法,而是先试探地使用 CAS 获取了一下锁,CAS 操作失败再调用 acquire 方法。这样设计可以提升性能。由于可能很多时候我们能在第一次试探获取时成功,而不需要再经过 acquire => tryAcquire => nonfairAcquire
的调用链。
只实现 lock 和 tryAcquire 两个方法
公平模式的 lock
直接调用 acquire,而没有像非公平模式先试图获取,由于这样可能导致违背“公平”的语义:在已等待在队列中的线程之前获取了锁。
acquire 是 AQS 的方法,表示先尝试取得锁,失败之后进入同步队列阻塞等待,介绍见本专栏的上一文
公平模式的 tryAcquire。不要授予访问权限,除非递归调用或者没有等待线程或者是第一个调用的。
该方法是 AQS 在 acquire 方法中留给子类去具体实现的
话不多说,看源码:
protected final boolean tryAcquire(int acquires) { // 获取当前的线程 final Thread current = Thread.currentThread(); // 获取 state 锁的状态 int c = getState(); // state == 0 => 尚无线程获取锁 if (c == 0) { // 判断 AQS 的同步对列里能否有线程等待,若没有则直接 CAS 获取锁 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { // 获取锁成功,设置独占线程 setExclusiveOwnerThread(current); return true; } } // 判断已经获取锁能否为当前的线程 else if (current == getExclusiveOwnerThread()) { // 锁的重入, 即 state 加 1 int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false;}
和 Sync 的 nonfairTryAcquire 方法实现相似,唯一不同的是当发现锁未被占用时,使用 hasQueuedPredecessors 确保了公平性。
会判断当前线程是不是属于同步队列的头节点的下一个节点(头节点是释放锁的节点)
public final boolean hasQueuedPredecessors() { // 这种方法的正确性取决于头在尾之前初始化和头初始化。假如当前线程是队列中的第一个线程,则next是准确的 Node t = tail; // 按反初始化顺序读取字段 Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }
执行非公平的 tryLock。
tryAcquire 是在子类中实现的,但是都需要对trylock 方法进行非公平的尝试。
final boolean nonfairTryAcquire(int acquires) { // 获取当前的线程 final Thread current = Thread.currentThread(); // 获取 AQS 中的 state 字段 int c = getState(); // state 为 0,表示同步器的锁尚未被持有 if (c == 0) { // CAS state 获取锁(这里可能有竞争,所以可能失败) if (compareAndSetState(0, acquires)) { // 获取锁成功, 设置获取独占锁的线程 setExclusiveOwnerThread(current); // 直接返回 true return true; } } // 判断现在获取独占锁的线程能否为当前线程(可重入锁的表现) else if (current == getExclusiveOwnerThread()) { // state 计数加1(重入获取锁) int nextc = c + acquires; if (nextc < 0) // 整型溢出 throw new Error("Maximum lock count exceeded"); // 已经获取 lock,所以这里不考虑并发 setState(nextc); return true; } return false;}
无参的 tryLock 调用的就是此方法
Lock 接口中定义的方法。
假如当前线程已经持有该锁,那么持有计数将添加1,方法返回true。
假如锁被另一个线程持有,那么这个方法将立即返回值false。
Lock lock = ...; if (lock.tryLock()) { try { // manipulate protected state } finally { lock.unlock(); } } else { // 执行可选的操作 }
提供了超时时间的入参,在时间内,仍没有得到锁,会返回 false
其中的 doAcquireNanos 已经实现好在 AQS 中。
释放锁,对于公平和非公平锁都适用
protected final boolean tryRelease(int releases) { // 释放 releases (因为可重入,这里的 c 不肯定直接为 0) int c = getState() - releases; // 判断当前线程能否是获取独占锁的线程 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; // 锁已被完全释放 if (c == 0) { free = true; // 无线程持有独占锁,所以置 null setExclusiveOwnerThread(null); } setState(c); return free;}
AQS 搭建了整个锁架构,子类锁的实现只要要根据场景,实现 AQS 对应的方法就可。