前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >面试系列之-AQS抽象同步器的核心原理(JAVA基础)

面试系列之-AQS抽象同步器的核心原理(JAVA基础)

作者头像
用户4283147
发布2023-09-11 15:55:42
1430
发布2023-09-11 15:55:42
举报
文章被收录于专栏:对线JAVA面试对线JAVA面试

CAS自旋实现的轻量级锁有两个大的问题:

(1)CAS恶性空自旋会浪费大量的CPU资源。

(2)在SMP架构的CPU上会导致“总线风暴”。

解决CAS恶性空自旋的有效方式之一是以空间换时间,较为常见的方案有两种:分散操作热点和使用队列削峰。JUC并发包使用的是队列削峰的方案解决CAS的性能问题,并提供了一个基于双向队列的削峰基类——抽象基础类AbstractQueuedSynchronizer(抽象同步器类,简称为AQS)。

AQS是CLH队列的一个变种,主要原理和CLH队列差不多。AQS队列内部维护的是一个FIFO的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向直接的前驱节点和直接的后继节点。所以双向链表可以从任意一个节点开始很方便地访问前驱节点和后继节点。每个节点其实是由线程封装的,当线程争抢锁失败后会封装成节点加入AQS队列中;当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。

代码语言:javascript
复制
//同步状态,使用 volatile保证线程可见
private volatile int state;
// 获取同步的状态
protected final int getState() {
    return state;
}
// 设置同步的状态
protected final void setState(int newState) {
    state = newState;
}
// 通过CAS设置同步的状态
protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

AQS中维持了一个单一的volatile修饰的状态信息state,AQS使用int类型的state标示锁的状态,可以理解为锁的同步状态。state因为使用volatile保证了操作的可见性,所以任何线程通过getState()获得状态都可以得到最新值。AQS提供了getState()、setState()来获取和设置同步状态。由于setState()无法保证原子性,因此AQS给我们提供了compareAndSetState()方法利用底层UnSafe的CAS机制来实现原子性。compareAndSetState()方法实际上调用的是unsafe成员的compareAndSwapInt()方法。

以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程执行该锁的lock()操作时,会调用tryAcquire()独占该锁并将state加1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(释放锁)为止,其他线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state能回到零态。

队列节点类:

代码语言:javascript
复制
static final class Node {
    /**节点等待状态值1:取消状态*/
    static final int CANCELLED = 1;
    /**节点等待状态值-1:标识后继线程处于等待状态*/
    static final int SIGNAL = -1;
    /**节点等待状态值-2:标识当前线程正在进行条件等待*/
    static final int CONDITION = -2;
    /**节点等待状态值-3:标识下一次共享锁的acquireShared操作需要无条件传播*/
    static final int PROPAGATE = -3;
    //节点状态:值为SIGNAL、CANCELLED、CONDITION、PROPAGATE、0
    //普通的同步节点的初始值为0,条件等待节点的初始值为CONDITION(-2)
    volatile int waitStatus;
    //节点所对应的线程,为抢锁线程或者条件等待线程
    volatile Thread thread;
    //前驱节点,当前节点会在前驱节点上自旋,循环检查前驱节点的waitStatus状态
    volatile Node prev;
    //后继节点
    volatile Node next;
    //若当前Node不是普通节点而是条件等待节点,则节点处于某个条件的等待队列上
    //此属性指向下一个条件等待节点,即其条件队列上的后继节点
    Node nextWaiter;
    ...
}

1.waitStatus属性

每个节点与等待线程关联,每个节点维护一个状态waitStatus,waitStatus的各种值以常量的形式进行定义。waitStatus的各常量值具体如下:

(1)static final int CANCELLED=1

waitStatus值为1时表示该线程节点已释放(超时、中断),已取消的节点不会再阻塞,表示线程因为中断或者等待超时,需要从等待队列中取消等待。

由于该节点线程等待超时或者被中断,需要从同步队列中取消等待,因此该线程被置1。节点进入了取消状态,该类型节点不会参与竞争,且会一直保持取消状态。

(2)static final int SIGNAL=1

waitStatus为SIGNAL(-1)时表示其后继的节点处于等待状态,当前节点对应的线程如果释放了同步状态或者被取消,就会通知后继节点,使后继节点的线程得以运行。

(3)static final int CONDITION=2

waitStatus为-2时,表示该线程在条件队列中阻塞(Condition有使用),表示节点在等待队列中(这里指的是等待在某个锁的CONDITION上,关于CONDITION的原理后面会讲到),当持有锁的线程调用了CONDITION的signal()方法之后,节点会从该CONDITION的等待队列转移到该锁的同步队列上,去竞争锁(注意:这里的同步队列就是我们讲的AQS维护的FIFO队列,等待队列则是每个CONDITION关联的队列)。

节点处于等待队列中,节点线程等待在CONDITION上,当其他线程对CONDITION调用了signal()方法后,该节点从等待队列中转移到同步队列中,加入对同步状态的获取中。

(4)static final int PROPAGATE=3

waitStatus为-3时,表示下一个线程获取共享锁后,自己的共享状态会被无条件地传播下去,因为共享锁可能出现同时有N个锁可以用,这时直接让后面的N个节点都来工作。这种状态在CountDownLatch中使用到了。

为什么当一个节点的线程获取共享锁后,要唤醒后继共享节点?共享锁是可以多个线程共有的,当一个节点的线程获取共享锁后,必然要通知后继共享节点的线程也可以获取锁了,这样就不会让其他等待的线程等很久,这种向后通知(传播)的目的也是尽快通知其他等待的线程尽快获取锁。

(5)waitStatus为0

waitStatus为0时,表示当前节点处于初始状态。Node节点的waitStatus状态为以上5种状态的一种。

2.thread成员

Node的thread成员用来存放进入AQS队列中的线程引用;Node的nextWaiter成员用来指向自己的后继等待节点,此成员只有线程处于条件等待队列中的时候使用。

FIFO双向同步队列:

AQS的内部队列是CLH队列的变种,每当线程通过AQS获取锁失败时,线程将被封装成一个Node节点,通过CAS原子操作插入队列尾部。当有线程释放锁时,AQS会尝试让队头的后继节点占用锁。AQS通过内置的FIFO双向队列来完成线程的排队工作,内部通过节点head和tail记录队首和队尾元素。

代码语言:javascript
复制
/*首节点的引用*/
private transient volatile Node head;
/*尾节点的引用*/
private transient volatile Node tail;

AQS的首节点和尾节点都是懒加载的。懒加载的意思是在需要的时候才真正创建。只有在线程竞争失败的情况下,有新线程加入同步队列时,AQS才创建一个head节点。head节点只能被setHead()方法修改,并且节点的waitStatus不能为CANCELLED。尾节点只在有新线程阻塞时才被创建。

JUC显式锁与AQS的关系:

AQS是java.util.concurrent包的一个同步器,它实现了锁的基本抽象功能,支持独占锁与共享锁两种方式。该类是使用模板模式来实现的,成为构建锁和同步器的框架,使用该类可以简单且高效地构造出应用广泛的同步器(或者等待队列)。

java.util.concurrent.locks包中的显式锁如ReentrantLock、ReentrantReadWriteLock,线程同步工具如Semaphore,异步回调工具如FutureTask等,内部都使用了AQS作为等待队列。

ReentrantLock与AQS的组合关系:

ReentrantLock是一个可重入的互斥锁,又称为“可重入独占锁”。ReentrantLock锁在同一个时间点只能被一个线程锁持有,而可重入的意思是,ReentrantLock锁可以被单个线程多次获取。

ReentrantLock的显式锁操作是委托(或委派)给一个Sync内部类的实例来完成的。而Sync内部类只是AQS的一个子类,ReentrantLock为了支持公平锁和非公平锁两种模式,为Sync又定义了两个子类NonfairSync和FairSync, 所以本质上ReentrantLock的显式锁操作是委托(或委派)给AQS完成的。一个ReentrantLock对象的内部一定有一个AQS类型的组合实例,二者之间是组合关系。显式锁与AQS之间的组合关系:

聚合关系的特点是,整体由部分构成,但是整体和部分之间并不是强依赖的关系,而是弱依赖的关系,也就是说,即使整体不存在了,部分仍然存在。例如,一个部门由多个员工组成,如果部门撤销了,人员不会消失,人员依然存在。

组合关系的特点是,整体由部分构成,但是整体和部分之间是强依赖的关系,如果整体不存在了,部分也随之消失。例如,一个公司由多个部门组成,如果公司不存在了,部门也将不存在。

由于显式锁与AQS之间是一种强依赖的聚合关系,如果显式锁的实例销毁,其聚合的AQS子类实例也会被销毁,因此显式锁与AQS之间是组合关系。AQS中的钩子方法:

自定义同步器时,AQS中需要重写的钩子方法大致如下:

·tryAcquire(int):独占锁钩子,尝试获取资源,若成功则返回true,若失败则返回false。

·tryRelease(int):独占锁钩子,尝试释放资源,若成功则返回true,若失败则返回false。

·tryAcquireShared(int):共享锁钩子,尝试获取资源,负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。

·tryReleaseShared(int):共享锁钩子,尝试释放资源,若成功则返回true,若失败则返回false。

·isHeldExclusively():独占锁钩子,判断该线程是否正在独占资源。只有用到condition条件队列时才需要去实现它。

以上钩子方法的默认实现会抛出UnsupportedOperationException异常。除了这些钩子方法外,AQS类中的其他方法都是final类型的方法,所以无法被其他类继承,只有这几个方法可以被其他类继承。简单的独占锁实现:

代码语言:javascript
复制
public class SimpleMockLock implements Lock{
    //同步器实例
    private final Sync sync = new Sync();
        // 自定义的内部类:同步器
        // 直接使用 AbstractQueuedSynchronizer.state 值表示锁的状态
        // AbstractQueuedSynchronizer.state=1 表示锁没有被占用
        // AbstractQueuedSynchronizer.state=0 表示锁没已经被占用
        private static class Sync extends AbstractQueuedSynchronizer{
                //钩子方法
                protected boolean tryAcquire(int arg){
                    //CAS更新状态值为1
                    if (compareAndSetState(0, 1)){
                    setExclusiveOwnerThread(Thread.currentThread());
                    return true;
                }
                return false;
            }
        //钩子方法
        protected boolean tryRelease(int arg){
            //如果当前线程不是占用锁的线程
            if (Thread.currentThread() != getExclusiveOwnerThread()){
                //抛出非法状态的异常
                throw new IllegalMonitorStateException();
            }
            //如果锁的状态为没有占用
            if (getState() == 0){
                //抛出非法状态的异常
                throw new IllegalMonitorStateException();
            }
            //接下来不需要使用CAS操作,因为下面的操作不存在并发场景
            setExclusiveOwnerThread(null);
            //设置状态
            setState(0);
            return true;
        }
    }
        //显式锁的抢占方法
        @Override
        public void lock(){
            //委托给同步器的acquire()抢占方法
            sync.acquire(1);
        }
        //显式锁的释放方法
        @Override
        public void unlock(){
            //委托给同步器的release()释放方法
            sync.release(1);
        }
    // 省略其他未实现的方法
    }
}

SimpleMockLock仅仅实现了Lock接口的以下两种方法:

(1)lock()方法:完成显式锁的抢占。

(2)unlock()方法:完成显式锁的释放。

SimpleMockLock的锁抢占和锁释放是委托给Sync实例的acquire()方法和release()方法来完成的。SimpleMockLock的内部类Sync继承了AQS类,实际上acquire()、release()是AQS的两个模板方法。在抢占锁时,AQS的模板方法acquire()会调用tryAcquire(int arg)钩子方法;在释放锁时,AQS的模板方法release()会调用tryRelease(int arg)钩子方法。

内部类Sync继承AQS类时提供了以下两个钩子方法的实现:

(1)protected boolean tryAcquire(int arg):抢占锁的钩子实现。此方法将锁的状态设置为1,表示互斥锁已经被占用,并保存当前线程。

(2)protected boolean tryRelease(int arg):释放锁的钩子实现。此方法将锁的状态设置为0,表示互斥锁已经被释放。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-09-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 对线JAVA面试 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档