深入理解CAS与AQS

一、CAS

1、什么是CAS

首先,CAS是一种算法,不是锁
CAS,其实是个简称,全称是 Compare And Swap,对比之后交换数据

  • CAS的实现原理
    • 首先呢,我们通过它的全称就可以看出来,CAS先是对比数据,然后再执行具体的原子操作。
    • 几个参数介绍:
      在这里插入图片描述
      • this:Unsafe 对象本身,需要通过这个类来获取 value 的内存偏移地址
      • valueOffset:value 变量的内存偏移地址
        • 这个就是现在要获取更新的那个变量
          在这里插入图片描述
          由此可见,他获取的这个value是通过volatile来修饰的,所以说直接从主存中获取这个值
        • volatile的作用:
          • 保障了共享变量的可见性(我们要了解JMM,即java内存模型,这个特点的实现靠的是 内存一致性协议
          • 防止指令重排序(单例模式的一种双检索实现里面(DCL)应用的就是这个特性)
      • expect:期望更新的值(一开始获取的值)
      • update:要更新的最新值
    • 如果原子变量中的 value 值等于 expect,则使用 update 值更新该值并返回 true,否则返回 false。

      通俗来讲就是比如有一个值 int x = 0;要对x进行++操作,那么我就获取这个x的值,作为expect,进行++操作以后,x 等于1,往回更新的时候看看此时的x是不是还是0,如果还是原来的值,就更新x为1,如果此时x的值不是0,而是100,那么就是CAS失败,返回false

2、CAS进行原子操作的三大问题

  • ABA问题
    • 因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
    • 解决办法:使用版本号来进行控制。在变量前面 追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A
  • 循环时间长开销大
    • 这种情况就是在循环CAS的情况产生的一种问题。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
    • 解决办法就是可以给他设置一个上限值,超过这个值还没成功就返回false。
  • 只能保证一个共享变量的原子操作
    • 当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性。
    • 这个时候就可以用锁
    • 是把多个共享变量合并成一个共享变量来 操作

3、循环CAS

应用场景最多,他的意思就是我在执行一次CAS操作下,看看是否可以执行成功,如果执行不成功,那么我就可以获取最新的值,重复进行操作,直到成功为止。

二、AQS

1、核心思想

AQS全称为AbstractQueuedSynchronizer,它的核心思想就是线程请求一个共享资源,如果这个资源目前没有被锁定,是处于空闲状态,那么这个线程就占用这块资源,并将这块共享资源的状态设置为锁定状态。如果这块资源被占用,已经处于锁定状态,那么这个线程就会被分配到一个队列里面,等待被唤醒获取这个锁(AQS通过CLH队列锁来实现,这个队列是一个FIFO队列,即先进先出)。
在这里插入图片描述

2、具体实现

  • AQS使用一个int成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作
  • private volatile int state;//共享变量,使用volatile修饰保证线程可见性
  • compareAndSetState(int expect, int update) 通过CAS操作将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
    在这里插入图片描述

3、AQS 对资源的两种共享方式

  • 独占

    • 只有一个线程能执行,典型应用就是ReentrantLock(重入锁)
    • 分为公平锁非公平锁
      • 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
        • 符合线程请求的绝对时间顺序
      • 非公平锁:当线程获取锁的时候,先通过CAS操作去抢锁,如果没抢到再加入到队列中等待被唤醒。
        • 可能造成线程“饥饿”,但极少的线程切换,保证了其更大的吞吐量,性能更高。但是非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。
      • ReentrantLock
        • ReentrantLock 默认采用非公平锁,因为考虑获得更好的性能,通过 boolean 来决定是否用公平锁(传入 true 用公平锁) 在这里插入图片描述
        • 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。在这里插入图片描述
        • 非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。
        • 如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。
  • 共享

    • 就是多个线程可同时执行
    • 读写锁中的读锁
    • CountDownLatch(倒计时器)

4、AQS 的应用

  • ReentrantLock (上面已经讲解过了)
  • ReentrantReadWriteLock(读写锁)
    • 可以保证多个线程可以同时读,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。所以在读操作远大于写操作的时候,读写锁就非常有用了。
  • Semaphore(信号量)
  • CountDownLatch (倒计时器)
  • CyclicBarrier(循环栅栏)

猜你喜欢

转载自blog.csdn.net/qq_44922113/article/details/114324483