# 前言
Reentrant 是 Java 提供的同步锁,基于 AQS 实现了公平和非公平的锁。
# 使用示例
import java.util.concurrent.locks.ReentrantLock; | |
public class Test { | |
public static void main(String[] args) { | |
ReentrantLock lock = new ReentrantLock(); | |
Runnable runnable = () -> { | |
try { | |
lock.lock(); | |
System.out.println(Thread.currentThread().getName() + " 拿到了锁。"); | |
Thread.sleep(2000L); | |
} catch (InterruptedException e) { | |
// ... | |
} finally { | |
System.out.println(Thread.currentThread().getName() + " 将要释放锁。"); | |
lock.unlock(); | |
} | |
}; | |
new Thread(runnable, "thread1").start(); | |
new Thread(runnable, "thread2").start(); | |
} | |
} |
输出结果为:
thread1 拿到了锁。 | |
thread1 将要释放锁。 | |
thread2 拿到了锁。 | |
thread2 将要释放锁。 |
可以看出,thread1 和 thread2 不能同时拿到锁,thread2 要等 thread1 释放锁后才能获取到锁。
# 源码分析
ReentrantLock 实现了 Lock 接口,可以通过构造函数的参数指定是否公平锁,默认是非公平锁。公平锁需要按照请求获取锁的顺序来加锁,所以会消耗 CPU,也会降低吞吐量。
# Lock
Lock 接口提供了加锁和释放锁的方法。
- void lock () - 加锁
- void lockInterruptibly () - 加锁,可响应中断
- boolean tryLock () - 尝试加锁,轻量级操作
- boolean tryLock (long time, TimeUnit unit) - 尝试加锁,超时放弃,可响应中断
- void unlock () - 释放锁
- Condition newCondition () - 等待条件对象
# Sync
Sync 是一个抽象类,实现了 AQS,提供基础的同步控制。
# nonfairTryAcquire
非公平方式获取锁,不讲究先来后到,如果持有锁的线程已释放掉锁,直接抢锁。
final boolean nonfairTryAcquire(int acquires) { | |
// 当前线程 | |
final Thread current = Thread.currentThread(); | |
// 已获取到锁的数量 | |
int c = getState(); | |
// 没有其他线程获取到锁 | |
if (c == 0) { | |
// 修改 state,标识自己已抢到锁 | |
if (compareAndSetState(0, acquires)) { | |
// 设置当前线程占用锁的标志 | |
setExclusiveOwnerThread(current); | |
// 成功拿到锁 | |
return true; | |
} | |
} | |
// 当前线程已获取到锁 | |
else if (current == getExclusiveOwnerThread()) { | |
// 最新获取锁的数量 | |
int nextc = c + acquires; | |
// 溢出 | |
if (nextc < 0) | |
throw new Error("Maximum lock count exceeded"); | |
// 更新最新获取锁的数量 | |
setState(nextc); | |
// 重入,成功拿到锁 | |
return true; | |
} | |
// 拿锁失败 | |
return false; | |
} |
# tryRelease
最终返回的是是否已经完全释放掉锁。
protected final boolean tryRelease(int releases) { | |
// 最新锁的数量 | |
int c = getState() - releases; | |
// 持有锁的线程不是自己,不能释放锁 | |
if (Thread.currentThread() != getExclusiveOwnerThread()) | |
throw new IllegalMonitorStateException(); | |
// 是否已完全释放锁 | |
boolean free = false; | |
// 最新持有锁的数量为 0,说明本次释放会彻底释放掉锁,将锁的持有者设置为 null | |
if (c == 0) { | |
free = true; | |
setExclusiveOwnerThread(null); | |
} | |
// 更新最新持有锁的数量 | |
setState(c); | |
// 返回是否完全释放掉了锁 | |
return free; | |
} |
# NonfairSync
非公平锁直接使用 Sync 提供的基础方法来加锁和释放锁。
# FairSync
公平锁加锁时,与非公平锁唯一的区别就是,如果当前没有其他线程持有锁,会判断下队列中有没有已等待的线程,如果没有的话,才会抢锁,否则会进等待队列。释放锁的逻辑就和非公平锁一样了,都是更新 state 减去释放的数量。
# tryAcquire
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; | |
if (nextc < 0) | |
throw new Error("Maximum lock count exceeded"); | |
setState(nextc); | |
return true; | |
} | |
return false; | |
} |
# 场景说明
结合 AQS 来说明下,多线程场景下,对公平锁进行获取锁和释放锁的流程。
# ① 线程 a 获取锁
此时还没有其他线程来获取锁,所以可以直接获取到锁。
# ② 线程 b 获取锁
此时线程 a 正在持有锁,所以线程 b 无法直接获取到锁,需要加入等待队列。等待队列中没有节点,head 和 tail 也还没有初始化,需要初始化队列。线程 b 调用 LockSupport.park () 进入了等待状态。
# ③ 线程 a 释放锁
线程 a 释放掉了锁,将线程 b 唤醒,然后线程 b 从上次进入等待的位置开始继续获取锁。如果线程 b 在等待期间,线程被标记中断,那么是要到拿到锁之后才响应的中断。
# ④ 线程 b 释放锁
线程 b 直接释放锁,由于后面没有等待的线程了,也不需要唤醒后面的线程。
# 总结
ReentrantLock 之所以逻辑简单,是因为有强大的 AQS 支持,它只需要实现下对 state 的操作即可,如果更新 state 失败,没有获取或释放锁,AQS 会进行入队、等待和唤醒的操作。