文章目录
前言
这篇文章讨论JDK中提供的 AQS接口以及其中一个实现类ReentrantLock。文章根据《Java并发编程的艺术》这本书以及黑马的视频 黑马多线程 做的笔记。
1. AQS
1. 概念
-
概述:全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架
-
特点:
- 用 state 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁
ⅰ. getState - 获取 state 状态
ⅱ. setState - 设置 state 状态
ⅲ. compareAndSetState - cas 机制设置 state 状态
ⅳ. 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源
- 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList
- 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet
- 用 state 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁
子类主要实现这样一些方法(默认抛出 UnsupportedOperationException)
- tryAcquire
- tryRelease
- tryAcquireShared
- tryReleaseShared
- isHeldExclusively
//获取锁的姿势
// 如果获取锁失败
if (!tryAcquire(arg)) {
// 入队, 可以选择阻塞当前线程 aqs使用park unpark来恢复运行和禁止运行
}
//释放锁的姿势
// 如果释放锁成功
if (tryRelease(arg)) {
// 让阻塞线程恢复运行
}
2. 代码
我们先自己实现一个不可重入锁,然后再去看源码
@Slf4j
//自定义不可重入锁
class MyLoack implements Lock{
//独占锁, 同步器类
class MySync extends AbstractQueuedSynchronizer{
@Override
protected boolean tryAcquire(int arg) {
if(compareAndSetState(0, 1)){
//加上了锁,并设置 owner 为当前线程
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
//设置没有线程占用这个锁,exclusiveOwnerThread是独占模式同步的当前所有者
setExclusiveOwnerThread(null);
setState(0); //把 volatile变量的写放在后面,让前面的 setExclusiveOwnerThread对其他线程可见
return true;
}
@Override //是不是持有独占锁
protected boolean isHeldExclusively() {
return getState() == 1;
}
public Condition newCondition(){
return new ConditionObject();
}
}
private MySync sync = new MySync();
@Override //加锁
public void lock() {
sync.acquire(1);
}
@Override //加锁,可打断锁
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override //尝试获取锁
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override //尝试获取锁 参数:时间
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
@Override //解锁
public void unlock() {
//release会唤醒等待的线程
sync.release(1);
}
@Override //设置一个新的容器
public Condition newCondition() {
return sync.newCondition();
}
}
测试:
MyLoack lock = new MyLoack();
new Thread(()->{
lock.lock();
try {
log.debug("加锁成功");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
log.debug("解锁成功");
lock.unlock();
}
}, "t1").start();
new Thread(()->{
lock.lock();
try {
log.debug("加锁成功");
}finally {
log.debug("解锁成功");
lock.unlock();
}
}, "t2").start();
结果输出:
2. ReentrantLock 原理
1. 非公平锁实现原理
从构造器来看,ReentrantLock模式实现的是非公平的锁
1. 加锁流程
1. 成功流程
public void lock() {
sync.lock();
}
final void lock() {
//尝试把 state 从 0 改成 1
if (compareAndSetState(0, 1))
//成功就设置线程是当前线程
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
2. 失败流程
在上面的基础上当又有一个线程尝试获取锁的时候,compareAndSetState(0, 1)
就失败了,这时候进入 acquire(1)
的流程
public final void acquire(int arg) {
//首先tryAcquire再次尝试获取锁,如果获取失败了,进入acquireQueued流程
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这个方法中,首先tryAcquire
再次尝试获取锁,如果获取失败了,进入acquireQueued
流程,这个方法的流程就是创建一个节点加入等待队列中去,流程如下:
Thread-1 执行了
-
lock方法中CAS 尝试将 state 由 0 改为 1,结果失败
-
lock方法中进一步调用acquire方法,进入 tryAcquire 逻辑,这里我们认为这时 state 已经是1,结果仍然失败
-
接下来进入 acquire方法的addWaiter 逻辑,构造 Node 队列
- 图中黄色三角表示该 Node 的 waitStatus 状态,其中 0 为默认正常状态
- Node 的创建是懒惰的
- 其中第一个 Node 称为 Dummy(哑元)或哨兵,用来占位,并不关联线程
private Node addWaiter(Node mode) {
//创建节点,关联节点和当前线程
Node node = new Node(Thread.currentThread(), mode);
//获取尾节点
Node pred = tail;
//这一步和enq里面的实现差不多
if (pred != null) {
//将尾节点和当前节点进行一个连接,注意下面是一个双向链表来的
node.prev = pred;
//连接 node <-> tail
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//尾节点为空,说明队列还未初始化,需要初始化head节点并入队新节点
enq(node);
return node;
}
接下来,当前线程进入 acquire方法的 acquireQueued 逻辑
- acquireQueued 会在一个死循环中不断尝试获得锁,失败后进入 park 阻塞
- 如果自己是紧邻着 head(排第二位),那么再次 tryAcquire 尝试获取锁,我们这里设置这时 state 仍为 1,失败
- 进入 shouldParkAfterFailedAcquire 逻辑,将前驱 node,即 head 的 waitStatus 改为 -1,这次返回 false。-1 表示这个节点有责任唤醒它的后继节点
- shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued ,再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1,失败
- 当再次进入 shouldParkAfterFailedAcquire 时,这时因为其前驱 node 的 waitStatus 已经是 -1,这次返回 true
- 进入 parkAndCheckInterrupt, Thread-1 park(灰色表示已经阻塞)
final boolean acquireQueued(final Node node, int arg) {
//设置为 true
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//获取当前节点的前驱节点
final Node p = node.predecessor();
//判断如果前驱节点是头节点(占位节点),那么就会再尝试去获取一下锁
if (p == head && tryAcquire(arg)) {
//如果获取成功了锁
//如果是头节点就把当前节点设置为头,所以队列中可以看到就是第一个节点是
//工作的节点,第二个节点开始才是等待的节点,看上面的图也可以看出来
setHead(node);
p.next = null; //方便 GC 去回收旧的头节点
failed = false;
//返回 false,表示不能被打断
return interrupted;
}
//获取不到锁,下面是判断到底应不应该暂停这个线程
if (shouldParkAfterFailedAcquire(p, node) &&
//下面是将当前线程挂起,这时候就发现不但将线程挂起,还给
//中断标志位设置为了 true,如果使用 wait,sleep,join 方法
//的任意一种,直接抛异常
parkAndCheckInterrupt())
//这个方法就阻塞在这里了,等到前面的线程唤醒的时候再向下运行
interrupted = true;
}
} finally {
if (failed)
//这里只有抛出异常才可以进入
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取前驱节点的状态
int ws = pred.waitStatus;
//如果状态是 -1
if (ws == Node.SIGNAL)
//这个节点已经设置了要求释放信号的状态, 所以可以安全的暂停
return true;
if (ws > 0) {
//如果大于0,此时把当前的node弄到第一位去
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//等于0(一开始等于0),旧给头节点设置状态为 -1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
//这个方法里面就是调用了 park 方法,并设置 interrupted 标记为为true
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
2. 解锁流程
public void unlock() {
sync.release(1);
}
1. 成功解锁
现在假设很多个线程都竞争失败了,就变成下面这个样子
Thread-0 释放锁,进入 tryRealease 流程,如果成功
- 设置 exclusiveOwnerThread 为 null
- state = 0
- 如果当前队列头节点不为 null,并且 head 的 waitStatus = -1,进入 unparkSuccessor 流程: unparkSuccessor中会找到队列中离 head 最近的一个 Node(没取消的),unpark 恢复其运行,本例中即为 Thread-1
- 此时回到 acquireQueued 这个方法,线程继续运行再次进入 for 循环,然后进行加锁
public final boolean release(int arg) {
//这里我们先不考虑里面的细节
//直到里面设置了 exclusiveOwnerThread = null 并且设置了 stare 为0
if (tryRelease(arg)) {
//获取第一个节点
Node h = head;
//如果head不为null而且head的状态是0
if (h != null && h.waitStatus != 0)
//就唤醒下一个节点,然后在 acquireQueued 这个方法继续运行尝试加锁
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
/**
省略前面一段代码
*/
if (s != null)
//这里是调用了线程的 unpark 方法来唤醒的
LockSupport.unpark(s.thread);
}
如果加锁成功(没有其他i线程来和 Thread-1 竞争),会设置 (acquireQueued 方法中)
- exclusiveOwnerThread 为 Thread-1,state = 1
- head 指向刚刚 Thread-1 所在的 Node,该 Node 清空 Thread
- 原本的 head 因为从链表断开,而可被垃圾回收
2. 失败解锁
如果这时候有其它线程来竞争(非公平的体现),例如这时有 Thread-4 来了,如果不巧被 Thread-4 占了先
- Thread-4 被设置为 exclusiveOwnerThread,state = 1
- Thread-1 再次进入 acquireQueued 这个方法的流程再次获取,然后重写进入 park 阻塞
2. 可重入原理
static final class NonfairSync extends Sync {
// ...
//获取锁的可重入
// Sync 继承过来的方法, 方便阅读, 放在此处
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//首先获取状态
int c = getState();
//如果是0,表示还没有人获取到锁
if (c == 0) {
//那就从 0 改成 1
if (compareAndSetState(0, acquires)) {
//然后设置exclusiveOwnerThread为当前线程
setExclusiveOwnerThread(current);
return true;
}
}
// 如果已经获得了锁, 线程还是当前线程, 表示发生了锁重入
else if (current == getExclusiveOwnerThread()) {
// state++
//比如第二次,那么c就是1,这时候c+acquires就表示 state++
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
//释放锁的可重入
// Sync 继承过来的方法, 方便阅读, 放在此处
protected final boolean tryRelease(int releases) {
// state--,如果是重入的就减去一层 比如现在是 2,那么就是 2-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);
//如果是锁重入了,那么第一次返回false,直到锁重入次数减成0了才返回true表示释放成功
return free;
}
}
3. 可打断原理
1. 不可打断模式
不可打断模式:在此模式下,即使它被打断,仍会驻留在 AQS 队列中,等到获取锁的之后才能运行(继续运行,打断是指打断标记设置为 true,对于线程的运行是没有影响的)
我们看下面的代码:acquire 中尝试获取锁,然后进入了acquireQueued方法,这些都是前面说过的。我们看 acquireQueued 这个方法,在没有获取到锁的时候,就会被 park,当上一个线程执行完成之后就调用 unpark 方法唤醒了这个线程,然后这个线程向下执行 interrupted = true,但是 执行完了这个语句又会继续 for 循环一直等到获取到锁,这时候就可以进入 if (p == head && tryAcquire(arg))
语句返回 interrupted = true 到 acquire中,而到了acquire中调用 selfInterrupt()
继续尝生一个打断标记,因为 parkAndCheckInterrupt
这个方法中的 Thread.interrupted
会清除打断标记
// Sync 继承自 AQS
static final class NonfairSync extends Sync {
// ...
private final boolean parkAndCheckInterrupt() {
// 如果打断标记已经是 true, 则 park 会失效
LockSupport.park(this);
// interrupted 会清除打断标记,清除是为了下次还可以park,否则如果有打断标记是不可以park的
return Thread.interrupted();
}
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null;
failed = false;
// 还是需要获得锁后, 才能返回打断状态
return interrupted;
}
if (
shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()
) {
// 如果是因为 interrupt 被唤醒, 返回打断状态为 true
interrupted = true;
}
}
} finally {
if (failed)
cancelAcquire(node);
}
}
public final void acquire(int arg) {
if (
//这里能返回来进入if,证明上面的方法返回true了
!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
) {
// 如果打断状态为 true
selfInterrupt();
}
}
static void selfInterrupt() {
// 重新产生一次中断,这时候线程是如果正常运行的状态,那么不是出于sleep等状态,interrupt方法就不会报错
Thread.currentThread().interrupt();
}
}
2. 可打断模式
我们调用 acquireInterruptibly 这个方法而不是 acquire 方法来获取锁。这个方法里面 doAcquireInterruptibly 中如果获取不到锁还是会进入 park,而此时被其他线程唤醒之后就会直接抛出异常。所以如果在队列里面等待的时候被打断是会抛出异常的。
static final class NonfairSync extends Sync {
public final void acquireInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 如果没有获得到锁, 进入 ㈠
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
// ㈠ 可打断的获取锁流程
private void doAcquireInterruptibly(int arg) throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) {
// 在 park 过程中如果被 interrupt 会进入此
// 这时候抛出异常, 而不会再次进入 for (;;)
//所以这里如果在队列里面等待的时候被打断是直接抛出异常的
throw new InterruptedException();
}
}
} finally {
if (failed)
cancelAcquire(node);
}
}
}
4. 公平锁
1. 对比以下非公平锁
代码还是重入锁这段代码,可以看到非公平锁是不会去检查 AQS 队列的,而是直接 compareAndSetState。
// ㈢ Sync 继承过来的方法, 方便阅读, 放在此处
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 如果还没有获得锁,这是第一次进入
if (c == 0) {
// 尝试用 cas 获得, 这里体现了非公平性: 不去检查 AQS 队列
if (compareAndSetState(0, acquires)) {
//设置exclusiveOwnerThread为当前线程
setExclusiveOwnerThread(current);
return true;
}
}
// 如果已经获得了锁, 线程还是当前线程, 表示发生了锁重入锁
else if (current == getExclusiveOwnerThread()) {
// state++
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 获取失败, 回到调用处
return false;
}
2. 公平锁
tryAcquire方法中判断如果 AQS 队列没有前驱节点了,才允许当前线程去 CAS 获取锁
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
// AQS 继承过来的方法, 方便阅读, 放在此处
public final void acquire(int arg) {
if (
!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
) {
selfInterrupt();
}
}
// 与非公平锁主要区别在于 tryAcquire 方法的实现
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 先检查 AQS 队列中是否有前驱节点, 没有才去竞争
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 继承过来的方法, 方便阅读, 放在此处
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
// h != t 时(头不等于尾)表示队列中有 Node, 证明有结点,如果自己的线程不是在老二(权值最高),老大是占位用的。
return h != t &&
(
//表示队列中还有没有老二
(s = h.next) == null ||
//或者当前线程不是老二线程
s.thread != Thread.currentThread()
);
}
}
5. 条件变量实现原理
每个条件变量其实就对应着一个等待队列,其实现类是 ConditionObject
1. await 流程
开始 Thread-0 持有锁,调用 await,进入 ConditionObject(单链表) 的addConditionWaiter 流程 创建新的 Node 状态为 -2(Node.CONDITION),关联 Thread-0,加入等待队列尾部
接下来进入 AQS 的 fullyRelease 流程,释放同步器上的锁,并唤醒当前这个节点的后继节点
然后 Thread-0 unpark AQS 队列中的下一个节点,竞争锁,假设没有其他竞争线程,那么 Thread-1 竞争成功
park 阻塞 Thread-0
public final void await() throws InterruptedException {
//如果线程已经打断了,就抛出异常
if (Thread.interrupted())
throw new InterruptedException();
//把节点添加到 ConditionObject 队列中
Node node = addConditionWaiter();
//fullyRelease:释放这个线程上面所有的锁,fullyRelease考虑到了对重入锁的释放
int savedState = fullyRelease(node);
int interruptMode = 0;
//判断如果上面的节点是2而且是第一个
while (!isOnSyncQueue(node)) {
//当前线程就到这里阻塞住了
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
//单链表的实现
private Node addConditionWaiter() {
Node t = lastWaiter;
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
//创建一个节点,状态设置为2
Node node = new Node(Thread.currentThread(), Node.CONDITION);
//连接到单链表去
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
//释放所有的锁
final int fullyRelease(Node node) {
boolean failed = true;
try {
//这里是获取到状态,同时也是重入的次数
int savedState = getState();
//按重入的次数释放锁,在 failed 的过程中,会唤醒这个节点的后继节点
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
2. signal 流程
假设 Thread-1 要来唤醒 Thread-0
进入 ConditionObject 的 doSignal 流程,取得等待队列中第一个 Node,即 Thread-0 所在 Node
执行 transferForSignal 流程,将该 Node 加入 AQS 队列尾部,将 Thread-0 的 waitStatus 改为 0,Thread-3 的waitStatus 改为 -1。
Thread-1 释放锁,进入 unlock 流程,略
public final void signal() {
//首先判断调用 signal 的线程是不是锁的持有者,只有 owner 线程才有资格去唤醒
if (!isHeldExclusively())
//不是就直接抛异常
throw new IllegalMonitorStateException();
//找到队首的元素
Node first = firstWaiter;
//调用doSignal
if (first != null)
doSignal(first);
}
private void doSignal(Node first) {
do {
//找下一个元素,然后是 null,就把lastWaiter 设置为null,表示没有节点了
//firstWaiter= first.nextWaiter:把下一个节点赋值为第一个节点
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
//transferForSignal:把上面的节点转移到等待队列里面
//如果转移失败就还会去看有没有更多节点,如果有就再循环
//有时候在等待队列中的元素被打断,就会被取消,这时候就返回了false
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
//设置state = 0,为什么是0呢,因为加到AQS竞争队列里面最后一个元素通常是0
//其他元素会被改成 -1
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//enq方法就是把节点加入到 AQS 队列中,返回加入的这个节点的前驱节点,也就是
//原来的尾节点
Node p = enq(node);
//获取前驱节点的状态
int ws = p.waitStatus;
//然后尝试把前驱节点的状态改成-1,表示前驱节点有义务唤醒新加入的尾节点
//然后唤醒我们刚刚加入的节点,在上面的例子中
//p = Thread-3, 加入的节点 - Thread-0
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
如有错误,欢迎指出!!!