java原子类AtomicStampedReference

发布时间 2023-08-24 17:15:50作者: 邢帅杰

一、什么是CAS
CAS,compare and swap的缩写,中文翻译成比较并交换。
CAS 操作包含三个操作数,内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。
二、案例
public static int count = 0;
private final static int MAX_TREAD = 10;
public static AtomicInteger atomicInteger = new AtomicInteger(0);

/*CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。
        使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。
        当计数器的值为0时,表示所有的线程都已经完成一些任务,然后在CountDownLatch上等待的线程就可以恢复执行接下来的任务。*/
        CountDownLatch latch = new CountDownLatch(MAX_TREAD);
        //匿名内部类
        Runnable runnable =  new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    count++;
                    atomicInteger.getAndIncrement();
                }
                latch.countDown(); // 当前线程调用此方法,则计数减一
            }
        };
        //同时启动多个线程
        for (int i = 0; i < MAX_TREAD; i++) {
            new Thread(runnable).start();
        }
        latch.await(); // 阻塞当前线程,直到计数器的值为0
        System.out.println("理论结果:" + 1000 * MAX_TREAD);
        System.out.println("static count: " + count);
        System.out.println("AtomicInteger: " + atomicInteger.intValue());

理论结果:10000
static count: 9994
AtomicInteger: 10000
每次运行,atomicInteger 的结果值都是正确的,count++的结果却不对,原因就是AtomicInteger是原子性操作,线程安全的,count不是。

三、Java中的Atomic 原子操作包
JUC 并发包中原子类 , 都存放在 java.util.concurrent.atomic
根据操作的目标数据类型,可以将 JUC 包中的原子类分为 4 类:
基本原子类、数组原子类、原子引用类型、字段更新原子类
1. 基本原子类
基本原子类的功能,是通过原子方式更新 Java 基础类型变量的值。基本原子类主要包括了以下三个:
AtomicInteger:整型原子类。
AtomicLong:长整型原子类。
AtomicBoolean :布尔型原子类。
2. 数组原子类
数组原子类的功能,是通过原子方式更新数组里的某个元素的值。数组原子类主要包括了以下三个:
AtomicIntegerArray:整型数组原子类。
AtomicLongArray:长整型数组原子类。
AtomicReferenceArray :引用类型数组原子类。
3. 引用原子类
引用原子类主要包括了以下三个:
AtomicReference:引用类型原子类。
AtomicMarkableReference :带有更新标记位的原子引用类型。
AtomicStampedReference :带有更新版本号的原子引用类型。
AtomicStampedReference 通过引入“版本”的概念,来解决ABA的问题。
4. 字段更新原子类
字段更新原子类主要包括了以下三个:
AtomicIntegerFieldUpdater:原子更新整型字段的更新器。
AtomicLongFieldUpdater:原子更新长整型字段的更新器。
AtomicReferenceFieldUpdater:原子更新引用类型里的字段。

四、 AtomicInteger
1、常用的方法:
public final int get() 获取当前的值
public final int getAndSet(int newValue) 获取当前的值,然后设置新的值
public final int getAndIncrement() 获取当前的值,然后自增
public final int getAndDecrement() 获取当前的值,然后自减
public final int getAndAdd(int delta) 获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) 通过 CAS 方式设置整数值
AtomicInteger的增减操作都调用了Unsafe实例的方法。private static final Unsafe unsafe = Unsafe.getUnsafe();

五、Unsafe类
Unsafe 是位于 sun.misc 包下的一个类,Unsafe 提供了CAS 方法,直接通过native方式(封装 C++代码)调用了底层的 CPU 指令 cmpxchg。
Unsafe 提供的 CAS 方法,主要如下: 定义在 Unsafe 类中的三个 “比较并交换”原子方法
/*
@param o 包含要修改的字段的对象
@param offset 字段在对象内的偏移量
@param expected 期望值(旧的值)
@param update 更新值(新的值)
@return true 更新成功 | false 更新失败
*/
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update);
public final native boolean compareAndSwapInt( Object o, long offset, int expected,int update);
public final native boolean compareAndSwapLong( Object o, long offset, long expected, long update);

六、CAS的缺点
1. ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
JDK 提供了两个类 AtomicStampedReference、AtomicMarkableReference 来解决 ABA 问题。
2. 只能保证一个共享变量的原子操作。一个比较简单的规避方法为:把多个共享变量合并成一个共享变量来操作。
JDK 提供了 AtomicReference 类来保证引用对象之间的原子性,可以把多个变量放在一个 AtomicReference 实例后再进行 CAS 操作。
比如有两个共享变量 i=1、j=2,可以将二者合并成一个对象,然后用 CAS 来操作该合并对象的 AtomicReference 引用。
3. 循环时间长开销大。高并发下N多线程同时去操作一个变量,会造成大量线程CAS失败,然后处于自旋状态,导致严重浪费CPU资源,降低了并发性。
解决 CAS 恶性空自旋的较为常见的方案为:
3.1 分散操作热点,使用 LongAdder 替代基础原子类 AtomicLong。
3.2 使用队列削峰,将发生 CAS 争用的线程加入一个队列中排队,降低 CAS 争用的激烈程度。JUC 中非常重要的基础类 AQS(抽象队列同步器)就是这么做的。

七、以空间换时间:LongAdder
1. LongAdder 的原理
LongAdder 的基本思路就是分散热点, 如果有竞争的话,内部维护了多个Cell变量,每个Cell里面有一个初始值为0的long型变量, 不同线程会命中到数组的不同Cell(槽 )中,各个线程只对自己Cell(槽)中的那个值进行 CAS 操作。这样热点就被分散了,冲突的概率就小很多。
在没有竞争的情况下,要累加的数通过 CAS 累加到 base 上。
如果要获得完整的 LongAdder 存储的值,只要将各个槽中的变量值累加,得到最后的值即可。
LongAdder结构:
函数:sum():long; reset():void; sumThenReset():long; longValue():long; longAccumulate(long,LongBinaryOperator,boolean):void;
成员:Striped64:#base:volatile long; #cellsBusy:volatile int; #cells:volatile Cell[];
/**
* cell表,当非空时,大小是2的幂。
*/
transient volatile Cell[] cells;

/**
* 基础值,主要在没有争用时使用
* 在没有争用时使用CAS更新这个值
*/
transient volatile long base;

/**
* 自旋锁(通过CAS锁定) 在调整大小和/或创建cell时使用,
* 为 0 表示 cells 数组没有处于创建、扩容阶段,反之为1
*/
transient volatile int cellsBusy;
Striped64 内部包含一个 base 和一个 Cell[] 类型的 cells 数组 。 在没有竞争的情况下,要累加的数通过 CAS 累加到 base 上;如果有竞争的话,会将要累加的数累加到 Cells 数组中的某个 cell 元素里面。所以 Striped64 的整体值 value 为 base+ ∑ [0~n]cells 。
Striped64 的设计核心思路就是通过内部的分散计算来避免竞争,以空间换时间。 LongAdder的 base 类似于 AtomicInteger 里面的 value ,在没有竞争的情况,cells 数组为 null ,这时只使用 base 做累加;而一旦发生竞争,cells 数组就上场了。cells 数组第一次初始化长度为 2 ,以后每次扩容都是变为原来的两倍,一直到 cells 数组的长度大于等于当前服务器 CPU 的核数。为什么呢?同一时刻,能持有 CPU 时间片而去并发操作同一个内存地址的最大线程数,最多也就是 CPU 的核数。
在存在线程争用的时候,每个线程被映射到 cells[threadLocalRandomProbe & cells.length] 位置的 Cell 元素,该线程对 value 所做的累加操作,就执行在对应的 Cell 元素的值上,最终相当于将线程绑定到了 cells 中的某个 cell 对象上。

八、使用 AtomicStampedReference 解决 ABA 问题
JDK 的提供了一个类似 AtomicStampedReference 类来解决 ABA 问题。
AtomicStampReference 在 CAS 的基础上增加了一个 Stamp 整型 印戳(或标记),使用这个印戳可以来觉察数据是否发生变化,给数据带上了一种实效性的检验。
AtomicStampReference 的 compareAndSet 方法首先检查当前的对象引用值是否等于预期引用,并且当前印戳(Stamp)标志是否等于预期标志,如果全部相等,则以原子方式将引用值和印戳(Stamp)标志的值更新为给定的更新值。
1、AtomicStampReference 的构造器:
/**
* @param initialRef初始引用
* @param initialStamp初始戳记
*/
AtomicStampedReference(V initialRef, int initialStamp)
2、AtomicStampReference 的常用的几个方法如下:
public V getRerference() 引用的当前值
public int getStamp() 返回当前的"戳记"
public boolean weakCompareAndSet(V expectedReference,V newReference,int expectedStamp,int newStamp)
expectedReference 引用的旧值;newReference 引用的新值;expectedStamp 旧的戳记;newStamp 新的戳记;
案例:
boolean success = false;
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(1, 0);
int stamp = atomicStampedReference.getStamp();
success = atomicStampedReference.compareAndSet(1, 0, stamp, stamp + 1);
System.out.println("success:" + success + ";reference:" + "" + atomicStampedReference.getReference() + ";stamp:" + atomicStampedReference.getStamp());
//值与戳记都一致,修改成新的值和戳记:success:true;reference:0;stamp:1

stamp = 0;// 修改印戳,更新失败
success = atomicStampedReference.compareAndSet(0, 1, stamp, stamp + 1);
System.out.println("success:" + success + ";reference:" + "" + atomicStampedReference.getReference() + ";stamp:" + atomicStampedReference.getStamp());
//戳记不同,更新失败:success:false;reference:0;stamp:1
原文链接:https://blog.csdn.net/lwang_IT/article/details/121638089