AQS简单过一过


AQS底层原理


AQS(AbstractQueuedSynchronizer)是一个抽象同步队列,JUC(java.util.concurrent)中很多同步锁都是基于AQS实现的。

AQS的基本原理就是当一个线程请求共享资源的时候会判断是否能够成功操作这个共享资源,如果可以就会把这个共享资源设置为锁定状态,如果当前共享资源已经被锁定了,那就把这个请求的线程阻塞住,也就是放到队列中等待。

AQS三大核心组件


state变量:

  • AQS中有一个被volatile声明的变量用来表示同步状态
  • 提供了getState()setState()compareAndSetState()方法来修改state状态的值
// 返回同步状态的当前值
protected final int getState() {  
  return state;
}

// 设置同步状态的值
protected final void setState(int newState) { 
  state = newState;
}

// CAS操作修改state的值
protected final boolean compareAndSetState(int expect, int update) {
  return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

对共享资源的操作方式:

上面说了AQSJUC中很多同步锁的底层实现,锁也分很多种,有像ReentrantLock这样的独占锁,也有ReentrantReadWriteLock这样的共享锁,所以AQS中也必然是包含这两种操作方式的逻辑

1.独占式

  • 获取资源的时候会调用acquire()方法,这里面会调用tryAcquire()方法去设置state变量,如果失败的话就把当前线程放入一个Node中存入队列
public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
 }
  • 释放资源的时候是调用realase()方法,会调用tryRelease()方法修改state变量,调用成功后会去唤醒队列中Node里的线程,unparkSuccessor()方法就是判断当前state变量是否符合唤醒的标准,如果合适就唤醒,否则继续放回队列
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
  • 注意tryAcquire()tryRelease()方法在AQS中都是空的,前面说了JUC中很多同步锁都是基于AQS实现,所以加锁和释放锁的逻辑都还不确定,因此是要在这些同步锁中实现这两个方法
protected boolean tryAcquire(int arg) {
     throw new UnsupportedOperationException();
}

protected boolean tryRelease(int arg) {
     throw new UnsupportedOperationException();
}

2.共享式

  • 获取资源会调用acquireShared()方法,会调用tryAcquireShared()操作state变量,如果成功就获取资源,失败则放入队列
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}
  • 释放资源是调用releaseShared()方法,会调用tryReleaseShared()设置state变量,如果成功就唤醒队列中的一个Node里的线程,不满足唤醒条件则还放回队列中
public final boolean releaseShared(int arg) {
   if (tryReleaseShared(arg)) {
       doReleaseShared();
       return true;
   }
   return false;
}  
  • 和独占式一样,tryAcquireShared()tryReleaseShared()也是需要子类来提供
protected int tryAcquireShared(int arg) {
   throw new UnsupportedOperationException();
}

protected boolean tryReleaseShared(int arg) {
   throw new UnsupportedOperationException();
}

条件变量Condition

Condition中的signal()await()方法类似与notify()wait()方法,需要和AQS锁配合使用

public static void main(String[] args) throws InterruptedException {

    ReentrantLock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

    new Thread(()->{
            lock.lock();
            System.out.println(" t1 加锁");
            System.out.println("t1 start await");
            try {
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t1 end await");
            lock.unlock();
        }
    ,"AAA").start();

    new Thread(()->{
            lock.lock();
            System.out.println(" t2 加锁");
            System.out.println("t2 start signal");
            condition.signal();
            System.out.println("t2 end signal");
            lock.unlock();
        }
    ,"BBB").start();

}
  • AQS中的原理,上面lock.newCondition()其实是new一个AQSConditionObject内部类的对象出来,这个对象里面有一个队列,当调用await()方法的时候会存入一个Node节点到这个队列中,并且调用park()方法阻塞当前线程,释放当前线程的锁。而调用singal()方法则会移除内部类中的队列头部的Node,然后放入AQS中的队列中等待执行机会
  • 同样的,AQS并没有实现newCondition()方法,也是需要子类自己去实现

ReentrantLock 加锁和释放锁底层原理实现

先来看一段代码

ReentrantLock lock = new ReentrantLock();
try {
    lock.lock(); // 加锁

    // 业务逻辑代码

} finally {
    lock.unlock(); // 释放锁
}

这段代码加锁和释放锁到底会发生什么故事呢?

很简单在AQS 内部有一个核心变量 (volatile)state 变量其代表了加锁的状态,初始值为0。

另外一个重要的关键 OwnerThread 持有锁的线程,默认值为null ,如下图

初始状态

线程1过来通过lock.lock()方式获取锁,获取锁的过程就是通过CAS操作volatile 变量state 将其值从0变为1。

如果之前没有人获取锁,那么state的值肯定为0,此时线程1加锁成功将state = 1。

线程1加锁成功后还有一步重要的操作,就是将OwnerThread 设置成为自己。如下图线程1加锁过程。

加锁过程

其实到这大家应该对AQS有个大概认识了,说白了就是并发包下面的一个核心组件,其内部维持state变量、线程变量等核型的东西,来实现加锁和释放锁的过程。

大家有没有不管是ReentrantLock还是ReentrantReadWriteLock 等为什么都是Reentrant 开头呢?

从单词本身意思也能看出,Reentrant 可重入的意思 ,也就说其是一个可重入锁。

可重入锁?

就是你可以对一个 ReentrantLock 进行多次的lock() 和 unlock() 操作,也就是可以对一个锁加多次,叫做可重入锁。 来一段代码直观感受下。

ReentrantLock lock = new ReentrantLock();
try {
    lock.lock(); // 加锁1

    // 业务逻辑代码
    lock.lock() // 加锁2

    // 业务逻辑代码

    lock.lock() // 加锁3

} finally {
    lock.unlock(); // 释放锁3
    lock.unlock(); // 释放锁2
    lock.unlock(); // 释放锁1
}

注意:释放锁是由内到外依次释放的,不可缺少。

问题又来了?ReentrantLock 内部又是如何来实现的呢?

说白了!还是我们AQS这个核心组件帮我实现的,很 easy~ 上述两个核心变量 state 和 OwnerThread 还记得吧!

重入就是判断当前锁是不是自己加上的,如果是就代表自己可以在次上锁,每重入一次就是将state值加1。就是这么简单啦!!!

说完了可重入我们再来看看锁的互斥又是如何实现的尼?

此时线程2也跑过来想加锁,CAS操作尝试将 state 从0 变成 1, 哎呀!糟糕state已经不是0了,说明此锁已经被别人拿到了。

接着线程2想??? 这个锁是不是我以前加上的,瞅瞅 OwnerThread=线程1 哎! 明显不是自己上的 ,悲催加锁失败了~~~。来张图记录一下线程2的悲苦经历吧~~

争锁过程

可是线程2加锁失败又该何去何从呢?

线程2:想,要是有个地方让我休息下,等线程1释放锁后通知我下再来从新尝试上锁就好了。

这时我们的核心部件AQS又登场了!

AQS: OK! 好吧!那我就给你提供一个落脚地吧(CLH)进去待着吧!一会让线程1叫你。

线程2: 屁颠屁颠的就去等待区小憩一会去了。同样来张图记录下线程2高兴样。

CLH等待队列

此时线程1业务执行完了,开始释放锁

  • 将state值改为0
  • 将OwnerThread 设为null
  • 通知线程2锁我已经用完了,该你登场了

线程2一听,乐坏了!立马开始尝试获取取锁,CAS 尝试将 state 值设为 1 ,如果成功将OwnerThread设为自己 线程2。
此时线程2成功获取到了锁,再来张图瞅瞅。

获锁过程

总结:

用一句话总结下:AQS就是Java并发包下的一个基础组件,用来实现各种锁和同步组件的,其核心分为三个组件。

  • Volatile state 变量
  • OwnerThread 加锁线程
  • CLH 同步等待队列

等并发核心组件。


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