AQS 和CAS是什么

什么是AQS?        
普通人回答

普通人的回答 AQS 全称是 AbstractQueuedSynchronizer,它是 J.U.C 包中 Lock 锁的底层实现,可 以用它来实现多线程的同步器!

高手的回答

        AQS 是多线程同步器,它是 J.U.C 包中多个组件的底层实现,如 Lock、 CountDownLatch、Semaphore 等都用到了 AQS. 从本质上来说,AQS 提供了两种锁机制,分别是排它锁,和 共享锁。 排它锁,就是存在多线程竞争同一共享资源时,同一时刻只允许一个线程访问该共享资 源,也就是多个线程中只能有一个线程获得锁资源,比如 Lock 中的 ReentrantLock 重 入锁实现就是用到了 AQS 中的排它锁功能。 共享锁也称为读锁,就是在同一时刻允许多个线程同时获得锁资源,比如 CountDownLatch 和 Semaphore 都是用到了 AQS 中的共享锁功能。

        AQS 是多线程同步器,它是 JU.C包中多个组件的底层实现,如Lock、CountDownLatch、Semaphore 等都用到了 AQS。从本质上来说,AQS 提供了两种锁机制,分别是排他锁和共享锁。
        排他锁,就是在多线程竞争同一共享资源时,同一时刻只允许一个线程访问该共享资源。也就是多个线程中只能有一个线程获得锁资源,比如Lock中的ReentrantLock(重入锁)实现就用到了 AOS中的排他锁功能。
        共享锁也称为读锁,就是在同一时刻允许多个线程同时获得锁资源,比如CountDownLatch和Semaphore 都用到了 AOS 中的共享锁功能。

如何理解AQS的实现原理?

(1)一个 volatile 修饰的 state 变量,作为一个竞态条件。
(2)用双向链表结构维护的 FIFO 线程等待队列。


        它的具体工作原理是,多个线程通过对这个state 共享变量进行修改来实现竞态条件,竞争失败的线程加入FIFO队列并且阻塞,抢占到竞态资源的线程释放资源之后,后续的线程按照FIFO 顺序实现有序唤醒。
        AQS 里面提供了两种资源共享方式:一种是独占资源,同一个时刻只能有一个线程获得竞态资源,比如 ReentrantLock 就使用这种方式实现排他锁;另一种是共享资源,同一个时刻多个线程可以同时获得竞态资源,CountDownLatch或者Semaphore 就使用共享资源的方式,实现同时唤醒多个线程。

AQS为什么要使用双向链表?

        首先,双向链表有两个指针,一个指针指向前置节点,另一个指针指向后继节点。所以,双向链表支持在 〇(1)时间复杂度下找到前置节点。因此,在进行插入和删除操作的时候,双向链表要比单向链表简单、高效。
        从双向链表的特性来看,一般认为 AQS 使用双向链表有3个原因。
        第1个原因,没有竞争到锁的线程加入阻塞队列,并且阻塞等待的前提是,当前线程所在节点的前置节点是正常状态,这样设计是为了避免链表中存在异常线程导致无法唤醒后续线程的问题,如下图所示。

        所以,线程阻塞之前需要判断前置节点的状态,如果没有指针指向前置节点,就需要从 head节点开始遍历,性能非常低。
        第2个原因,在Lock 接口里面有一个lockInterruptiblyO方法,这个方法表示处于锁阻塞的线程允许被中断。
        如下图所示,没有竞争到锁的线程加入同步队列等待以后,是允许外部线程通过imnterupt0方法触发唤醒并中断的。这个时候,被中断的线程的状态会修改成CANCELLED。而被标记为CANCELLED 状态的线程,是不需要去竞争锁的,但是它仍然存在于双向链表里。这就意味着在后续的锁竞争中,需要把这个节点从链表里移除,否则会导致锁阻塞的线程无法被正常唤醒。在这种情况下,如果是单向链表,就需要从head节点开始往下逐个遍历,找到并移除异常状态的节点,同样效率也比较低,还会导致锁的唤醒操作和遍历操作之间的竞争。

        第3个原因,是为了避免线程阻塞和唤醒的开销,所以刚加入链表的线程,首先会通过自旋的方式尝试竞争锁,如下图所示。 

        但是实际上按照公平锁的设计,只有head节点的下一个节点才有必要去竞争锁,后续的节点竞争锁的意义不大。否则,就会造成羊群效应,也就是大量的线程在阻塞之前尝试去竞争锁带来比较大的性能开销。
        所以,为了避免这个问题,加入链表中的节点在尝试竞争锁之前,需要判断前置节点是不是head节点,如果不是head节点,就没必要再去触发锁竞争的动作。所以这里会涉及前置节点的查找,如果是单向链表,那么这个功能的实现会非常复杂。

什么是CAS?

        CAS是Java中 Unsafe 类里面的方法,它的全称是CompareAndSwap,是比较并交换的意思。它的主要功能是保证在多线程环境下,对于共享变量的修改的原子性。


        举个例子,有这样一个场景,有一个成员变量state,默认值是0,定义了一个方法doSomething(),这个方法的逻辑是,判断state是否为0,如果为0,就修改成1。这个逻辑看起来没有任何问题,但是在多线程环境下,会存在原子性问题,因为这里是一个典型的Read-Write操作。一般情况下,我们会在 doSomething()这个方法上加同步锁来解决原子性问题。

public class Example {
    private int state = 0;
    public void doSomething() {
        if (state == 0) {//在多线程环境中,存在原子性问题
            state = 1;//TODO
        }
    }
}

        但是,加同步锁会带来性能上的损耗,所以对于这类场景,我们就可以使用CAS来进行优化。下面是优化之后的代码,在 doSomething0方法中,我们调用了Unsafe 类中的compareAndSwapInt0)方法来达到同样的目的,这个方法有4个参数,分别是当前对象实例、成员变量state 在内存地址中的偏移量、预期值0和期望更改之后的值1。


public class Example {
    private volatile int state = 0;
    private static final Unsafe unsafe = unsafe.getUnsafe();
    private static final long stateOffset;

    static {
        try {
            stateoffset = unsafe.objectFieldoffset(Example.class.getDeclaredField("state”));"
        } catch (Exception ex) {
            throw new Error(ex);
        }
    }

    public void doSomething() {
        if (unsafe.compareAndSwapInt(this, state0ffset, 0, 1)) {//TODO
        }
    }
}

       CAS会比较 state 内存地址偏移量对应的值和传入的预期值0是否相等,如果相等,就直接修改内存地址中state 的值为1,否则返回 false,表示修改失败,而这个过程是原子的,不存在线程安全问题。CAS是一个 native方法,实际上它最终还是会面临同样的问题,就是先从内存地址中读取state 的值,然后去比较,最后修改。这个过程不管在什么层面上实现,都会存在原子性问题。所以,在CAS的底层实现中,在多核CPU环境下,会增加一个Lock指令对缓存或者总线加锁从而保证比较并替换这两个指令的原子性。
CAS主要用在并发场景中,比较典型的使用场景有两个。


1)J.U.C里Atomic 的原子实现,比如 AtomicInteger、AtomicLong。
2)实现多线程对共享资源竞争的互斥性质,比如AQS、ConcurrentHashMap、ConcurrentLinkedQueue等都有用到。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

missterzy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值