• 并发中 i++操作是非线程安全的,因i++ 不是原子操作。
  • 如何保证原子?常用就是加锁,java中可用Synchronized和CAS实现加锁效果。
  • Synchronized是典型的悲观锁,CAS是乐观锁。

    CAS

  • CAS(Compare-And-Swap)是比较并交换的意思,是一条CPU并发原语,用于判断内存中某个值是否为预期值,是则更改为新的值,过程是原子的

  • cas机制使用了三个基本操作数:内存地址V,旧的预期值A,计算要修改后的新值B。
    • 初始状态:内存地址V中存储着变量值为1.
    • 线程1想把内存地址为V的变量值增加1.这时对线程1来说,旧预期值A=1,要修改的新值B=2.
    • 在线程1要体校变更之前,线程2捷足先登,已经吧内存地址V的值改为2了。
    • 线程1开始提交更新,首先将预期值A和内存地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败。
    • 线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=2,B=3。这个重新尝试的过程被称为自旋。若多次失败会有多次自旋
    • 线程1再次提交更新,这一次没有其他线程改变地址V的值。线程1进行Compare,发现预期值A和内存地址V的实际值是相等的,进行Swap操作,将内存地址V的实际值修改为B。
    • 总结:更新时,只有当变量的预期值A和内存地址V中的实际值相同时,才将内存地址V对应的值修改为B,这个操作就是CAS。

CASDemo

  1. /**
  2. *
  3. * @author Skiray
  4. * @date 2021/6/30 1:10
  5. */
  6. public class CASDemo {
  7. public static void main(String[] args) {
  8. AtomicInteger atomicInteger = new AtomicInteger(233);
  9. // public final boolean compareAndSet(int expect, int update)
  10. // return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
  11. // expect:期望值;update更新值
  12. // 符合期望更新;CAS是计算机的底层原语
  13. // unsafe类native类,操作内存,java无法直接操作内存,C++可,java借native实现
  14. atomicInteger.compareAndSet(233,333);
  15. System.out.println(atomicInteger);
  16. atomicInteger.getAndIncrement();//加1
  17. }
  18. }
  • cas底层源码
    CAS 和 Volatile - 图1

image.png

  • 使用了do while循环,while条件里面就是cas比较并交换的结构,类似自旋锁,比较不成功一直循环。高并发若长时间不成功非常消耗cpu性能。

    CAS基本原理

  • cas包括两个操作,Compare和Swap,是一种系统原语,由若干指令组成。原语的执行必须是连续的,不能中断。是一条CPU原子指令,由操作系统硬件保证。

  • cas5引入,在sun.misc.Unsafe类中定义了CAS相关方法。
  • JUC下的Atomic包封装了cas,如AtomicINteger类就可解决 i++非原子问题,通过源码可知是通过vilatile关键字和CAS操作实现的。

    CAS缺点

  • 自旋锁,循环会耗时

  • 一次只能保证一个共享变量的原子性
  • ABA问题
  • cpu开销
  • 只能保证一个变量原子操作

image.png

ABA问题

  • ABA问题:假设有一初始值为A修改后为B,然后又修改为A,这个修改CAS无法感知到。若是整型结果一样,若是对象引用类型包含多个变量,引用没有改变实际上包含的变量已经被改变,会出问题。
    • ABA解决:变量加版本号
    • 5提供的AtomicStampedReference类,他的CompareAndSet方法先检查当前引用和标志是否等于预期引用和标志,若全相等,则以原子方式将该引用和标志值设置为给定的更新值。
  • 自旋开销问题:CAS出现冲突首先会自旋,若竞争激烈自旋长时间不成功,会给cpu带来很大的额外开销。
    • 自旋解决:现在自旋次数,避免过度消耗cpu,还可考延迟执行。
  • 只能保证单个变量的原子性:当对一个共享变量执行操作,cas保证原子性,但若对多个共享变量进行操作时,cas无法保证原子性如 i++;j++

    • 解决:synchronized加锁;将多个变量操作合成一个变量操作,AtomicReference类保证引用对象之间的原子性,可把多个变量放在一个对象来进行CAS操作。
  • 解决ABA:原子引用,乐观锁

    1. public class CASDemo {
    2. public static void main(String[] args) {
    3. // 解决ABA
    4. // public AtomicStampedReference(V initialRef, int initialStamp)
    5. // initialRef:初始值;initialStamp:时间戳
    6. AtomicStampedReference<Integer> reference = new AtomicStampedReference<>(122,1);
    7. // 乐观锁--版本号判断时间戳
    8. // Integer,包装类,使用了对象缓存机制,范围-128,127超过就会开辟新内存空间
    9. // 范围内的IntegerCache.cache产生,复用已有对象
    10. reference.compareAndSet(122,111, reference.getStamp(),reference.getStamp()+1);
    11. }
    12. }

参考
参考


Volatile

  • 内存可见
  • 指令重排

为什么要弄一个CPU高速缓存呢?类比我们开发网站后台系统使用的缓存(比如 Redis)是为了解决程序处理速度和访问常规关系型数据库速度不对等的问题。CPU缓存则是为了解决CPU处理速度和内存处理速度不对等的问题。我们甚至可以把内存可以看作外存的高速缓存,程序运行的时候我们把外存的数据复制到内存,由于内存的处理速度远远高于外存,这样提高了处理速度。总结:CPU Cache 缓存的是内存数据用于解决CPU处理速度和内存不匹配的问题,内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题。为了更好地理解,我画了一个简单的CPU Cache示意图如下(实际上,现代的CPU Cache通常分为三层,分别叫L1,L2,L3 Cache):privatevolatilestaticSingletonuniqueInstance;
CAS 和 Volatile - 图4
CPU Cache的工作方式:先复制一份数据到 CPU Cache中,当CPU需要用到的时候就可以直接从CPU Cache中读取数据,当运算完成后,再将运算得到的数据写回Main Memory 中。但是,这样存在内存缓存不一致性的问题!比如我执行一个 i++操作的话,如果两个线程同时执行的话,假设两个线程从CPUCache中读取的i=1,两个线程做了1++运算完之后再写回 Main Memory之后 i=2,而正确结果应该是 i=3。CPU 为了解决内存缓存不一致性问题可以通过制定缓存一致协议或者其他手段来解决。JMM(Java内存模型)在 JDK1.2 之前,Java 的内存模型实现总是从主存(即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷⻉,造成数据的不一致。
CAS 和 Volatile - 图5
要解决这个问题,就需要把变量声明为volatile,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。所以,volatile 关键字除了防止 JVM 的指令重排,还有一个重要的作用就是保证变量的可⻅性。

AQS理论的数据结构

  • AQS内部有3个对象,一个是state(用于计数器,类似gc的回收计数器),一个是线程标记(当前线程是谁加锁的),一个是阻塞队列。