图解AQS原理(ReentrantLock实现)


一、什么是AQS?

具体可参考文章《什么是AQS?》

二、AQS原理

在AQS内部,维护了:

  • state变量:用volatile int修饰,当state大于0的时候说明对象锁已经被占有了,state的修改动作是通过CAS来完成的
  • 等待队列:即CLH队列(FIFO),用来实现线程的排队工作,当线程加锁失败时,该线程会封装成一个Node节点放在队列尾部
  • Node节点:存放了int waitStatusNode prevNode nextThread thread

1、state变量

state变量由volatile int修饰,并提供多个方法使用,大概代码如下:

// 共享变量,使用volatile修饰保证线程可见性
private volatile int state;

//返回同步状态的当前值
protected final int getState() {
    return state;
}
// 设置同步状态的值
protected final void setState(int newState) {
    state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

2、等待队列

如图所示,对于某一资源,每一线程会先添加到等待队列,再按照顺序去尝试获得资源的对象锁。

如果当前线程所在的Node节点获得锁,那么它会释放前驱节点,将head指向当前占有锁的线程所在的Node节点。

三、图解AQS原理(ReentrantLock实现+非公平锁)

本文主要是来模拟多个线程通过竞争锁、释放锁的场景来解释AQS原理(ReentrantLock实现),具体场景为:三个线程同时竞争锁、释放锁

步骤1:线程一加锁成功,线程二和线程三加锁失败

三个线程并发加锁,结果如下:

  • 线程一加锁成功(通过CASstate0改为1
  • 线程二线程三加锁失败,加入到FIFO队列

此时AQS队列同步器数据如下:

  • 线程一通过CASstate0改为1,并设置当前独占线程exclusiveOwnerThread线程一

此时FIFO队列数据如下:

  • 线程二线程三依次添加到FIFO队列的队尾
  • 双向链表设计:如线程二后置节点是线程三线程三前驱节点是线程二
  • 设置head为第一个节点、tail为最后一个节点
  • 具体为什么线程二之前还有一个节点?后续源码分析再说。。。

步骤2:线程一释放锁,线程二尝试加锁

结果如下:

  • 线程一释放锁,并唤醒head节点的后置节点(即线程二
  • 线程二尝试加锁,并成功加锁

线程一释放锁后,线程二加锁前的数据如下:

  • 线程一释放锁,state置为0(代表可被加锁),并设置当前独占线程exclusiveOwnerThread为null
  • head节点的后置节点(即线程二)被唤醒

唤醒线程二后的数据如下:

  • 线程二被唤醒后,通过CAS尝试获得锁(如果没获得就继续被挂起)
  • 修改head为当前节点(即线程二),并回收原head节点

步骤3:线程二释放锁,线程三尝试加锁

同理步骤2

四、非公平锁有什么问题?什么是公平锁?

像上面的图解是基于非公平锁来实现的,也是ReentrantLock的默认实现。

什么是公平锁?什么是非公平锁?: 简单来说,公平锁就是线程按顺序获得锁(先等待先获得),非公平锁就是随机获得锁(可能造成某个线程等待太久)。

1、非公平锁带来的问题

我们先来看下非公平锁的流程:

正常步骤

  1. 线程二释放锁,并唤醒线程三
  2. 线程三尝试加锁成功
  3. 线程四尝试加锁失败,并加入到FIFO队列

异常步骤

  1. 线程二释放锁,并唤醒线程三
  2. 线程四尝试加锁成功
  3. 线程三尝试加锁失败,仍然挂起

解析:当释放锁的线程唤醒FIFO队列的线程去尝试加锁时,被新加入的线程抢先加锁并成功了,导致FIFO队列的线程尝试加锁失败并继续挂起

2、公平锁

我们先来看下公平锁的流程:

步骤

  1. 线程二释放锁,并唤醒线程三
  2. 线程四判断FIFO队列是否为空,如果为空则尝试加锁,如果不为空则加入
  3. 线程三尝试加锁成功

解析公平锁解决了非公平锁中出现的非顺序获得锁的问题(具体实现是通过分开实现tryAcquire()方法,具体后面源码分析再说。。。


文章作者: GaryLee
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 GaryLee !
  目录