Java的atomic包下的CAS原子操作类原理

前言

java的高并发多线程,想让程序保持原子操作分别有两种方式,就是CAS 与 Synchronized 。
这两种的使用情景如下:   
对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态与内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。

Java内存模型(Java Memory Model)

1、简介:想要了解CAS底层原理,则先知道内存模型,内存模型本身是一个抽象的概念,并不在内存中真实存在的,它描述的是一组规范或者规则,通过这组规范定义了程序中各个变量(实例字段,静态字段和构成数组对象的元素)的访问方式.。

2、工作原理:在jvm中有一个主内存(main memory),而每个线程都有自己的工作内存(working memory),一个线程对一个可变属性(variable)进行操作的时候,会先在自己的工作内存里面建立一个复制属性,操作完成之后再写入主内存,如果有多个线程同时操作同一个可变属性,就可能会出现不可预知的结果,所以线程安全就是为了避免这种情况的发生,如下图:
在这里插入图片描述

3、java中的volatile关键字作用:
  1.volatile可以保证变量的可见性。
  2.保证有序性。(防止指令重排)。
  【当一个线程修改了共享变量的值,新的值会立刻同步到主内存当中。而其他线程读取这个变量的时候,也会强制从主内存中拉取最新的变量值。】但是volatile不能保证原子性。因此对于同步最终还是要回到锁机制上来。在java中,确保线程安全的方法有两种:一种是使用内置锁(synchronized),一种是使用原子类(java.util.concurrent包下的),对于原子类,它所用的机制就是CAS机制。

CAS核心思想

1、一个 CAS 涉及到以下操作: 假设内存中的原数据V,旧的预期值A,需要修改的新值B:
比较 A 与 V 是否相等。(比较) 如果比较相等,将 B 写入 V。(交换) 返回操作是否成功。 当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。可见 CAS 其实是一个乐观锁。

2、再举个例子如下图中,主存中保存A变量值,线程中要使用A值要先从主存中读取A值到线程的工作内存A变量中,然后计算后变成C值,最后再把B值写回到主内存A变量中。多个线程共用A值都是如此操作。CAS的核心是在将C值写入到主内存的A之前要比较主内存A值和线程的工作内存A值是否相同,如果不相同证明此时A值已经被其他线程改变,重新将主内存A值赋给线程工作内存的A变量,线程并重新计算得到工作内存的C值,如果相同,则将工作内存的C值赋给主内存的A变量。
![![在这里插入图片描述](https://img-blog.csdnimg.cn/b73d456712a74851a04254f9fe9aba46.png](https://img-blog.csdnimg.cn/9574fc73c04b4afea89a4a80d7e5e6fd.png

AtomicReference类

UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个 volatile 变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。如下图:
在这里插入图片描述

//比较和设置新对象操作
public final boolean compareAndSet(V expect, V update) {
		//开始调用native层代码,如果当前值==期望值,则原子地将值设置为给定的更新值。
        return U.compareAndSwapObject(this, VALUE, expect, update);
    }
//原子地设置为给定值并返回旧值。
public final V getAndSet(V newValue) {
        return (V)U.getAndSetObject(this, VALUE, newValue);
    }

java.util.concurrent.atomic包下的类

在JUC中原子类可以分成四类
在这里插入图片描述

基本类型

使用原子的方式更新基本类型

AtomicInteger:整形原子类

AtomicLong:长整型原子类

AtomicBoolean:布尔型原子类

数组类型

使用原子的方式更新数组里的某个元素

AtomicIntegerArray:整形数组原子类

AtomicLongArray:长整形数组原子类

AtomicReferenceArray:引用类型数组原子类

引用类型

AtomicReference:引用类型原子类

AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。

AtomicMarkableReference :原子更新带有标记位的引用类型

对象的属性修改类型

AtomicIntegerFieldUpdater:原子更新整形字段的更新器

AtomicLongFieldUpdater:原子更新长整形字段的更新器

AtomicReferenceFieldUpdater:原子更新引用类型字段的更新器

AtomicInteger 类的原理:
AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。

CAS机制导致的问题及解决方案

1、ABA问题:
线程1取出A之后被阻塞了,此时线程2把内存中A改为B,一系列操作后又改为A,此时线程1恢复执行,取内存中的A与手中的A做比较,发现没有变,继续执行。

然而此时俩A虽然可能是一样的,但是其实是被修改过的。例如,线程1需要替换的是一个栈顶,从A替换成B,但是执行前线程2抢占到时间片,对栈做出了一系列出栈操作,然后又将A入栈,此时线程1恢复,发现栈顶还是A,所以替换成B,但此时这个栈已经不是原来的栈了。

解决思路——版本号:
在比较的时候加入版本号的比较,每次修改时也修改版本号。
1.5开始的AtomicStampedReference就是采用了的版本号比较。

2、执行开销大:
CAS如果长时间不成功会一直自旋循环,会产生不少的执行开销。并且为了自旋结束时避免内存顺序冲突,CPU会对流水线进行重排,这样会严重影响cpu性能。

解决思路——pause指令:

pause指令能让自旋失败时cpu睡眠一小段时间再继续自旋,从而使得读操作的频率低很多,为解决内存顺序冲突而导致的流水线重排的代价也会小很多。

内存顺序冲突——当自旋锁快要释放的时候,持锁线程会有一个store命令,外面自旋的线程会发出各自的load命令,而此处并没任何 happen-before 排序,所以处理器是乱序执行,所以为了避免load出现在store之前此时会进行流水线清空再重排序,会严重影响cpu效率,Pause指令的作用就是减少并行load的数量,从而减少重排序时所耗时间。(不懂load和store可以去看看JMM(java内存模型)的资料)

猜你喜欢

转载自blog.csdn.net/weixin_44715716/article/details/127009399