Java并发编程之CAS,原子包Atomic

1. 什么是CAS?

CAS 的英文全称为 Compare and Swap,翻译成中文为“比较并交换”。

1.1 CAS详解

CAS 是一种无锁算法,该算法关键依赖两个值——期望值(旧值)和新值,底层 CPU 利用原子操作,判断内存原值与期望值是否相等,如果相等则给内存地址赋新值,否则不做任何操作。使用 CAS 进行“无锁编程”(Lock Free)的步骤大致如下:

  1.获得字段的期望值(oldValue)。
  2.计算出需要替换的新值(newValue)。
  3.通过 CAS 将新值(newValue)放在字段的内存地址上,如果 CAS 失败则重复第 1 步到第 2 步,一直到 CAS 成功,这种重复俗称 CAS 自旋。

使用CAS进行“无锁编程”的伪代码,如下:

do {
     获得字段的期望值(oldValue);
     计算出需要替换的新值(newValue);
 } while (!CAS(内存地址,oldValue,newValue))

举一个简单的例子对上述代码进行说明:
       假如某个内存地址(某对象的属性)的值为 100,现在有两个线程(线程 A、线程 B)使用CAS 无锁编程对该内存地址进行更新,线程 A 欲将其值更新为 200,线程 B 欲将其值更新为 300,如图所示:


两条线程 A、B 需要对同一个内存地址进行更新

       由于线程是并发执行,谁都有可能先执行。但是 CAS 是原子操作,对同一个内存地址的 CAS操作在同一时刻只能执行一个。所以在这个例子中,要么线程 A 先执行,要么线程 B 先执行。假设线程 A 的 CAS(100,200)执行在前,由于内存地址的旧值 100 与该 CAS 的期望值 100 相等,所以线程 A 会操作成功,内存地址的值被更新为 200。
线程 A 执行成功 CAS(100,200)之后,内存地址的值如图所示:


线程 A 执行 CAS(100,200)成功之后内存地址的值

       接下来执行线程 B 的 CAS(100,300)操作,此时内存地址的值为 200,不等于 CAS 的期望值 100,线程 B 操作失败。线程 B 只能自旋,开始新的循环,这一轮循环首先获取到内存地址的值 200,然后进行 CAS(200,300)操作,这一次内存地址的值与 CAS 的预期值(oldValue)相等,线程 B 操作成功。
       当 CAS 进行内存地址的值与预期值比较时,如果相等,则证明内存地址的值没有被修改,可 以替换成新值,然后继续往下运行;如果不相等,说明明内存地址的值已经被修改,放弃替换操作,然后重新自旋。当并发修改的线程少,冲突出现的机会少时,自旋的次数也会很少,CAS 性能会很高;当并发修改的线程多,冲突出现的机会高时,自旋的次数也会很多,CAS 性能会大大降低。所以,提升 CAS 无锁编程的效率,关键在于减少冲突的机会。

1.2 操作系统中的CAS

       操作系统层面的CAS一条CPU的原子指令(cmpxchg指令),正是由于该指令具备了原子性,所以CAS操作不会造成数据不一致的问题,Unsafe提供的CAS方法,直接通过native方式(封装C++代码)调用了底层CPU指令cmpxchg。

2. Unsafe类

       Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全的底层操作。如直接访问系统内存资源、自主管理内存资源等,Unsafe 大量的方法都是 native 方法,基于 C++语言实现,这些方法在提升 Java 运行效率、增强 Java 语言底层资源操作能力方面起到了很大的作用。
为什么此类取名为 Unsafe 呢?
       我们都知道Unsafe有不安全的意思,由于使用 Unsafe 类可以像 C 语言一样使用指针操作内存空间,这无疑增加了指针相关问题、内存泄露问题的出现概率。总之,在程序中过度使用 Unsafe 类,会使得程序出错的概率变大,使得安全的语言 Java 变得不再“安全”,因此对 Unsafe 的使用一定要慎重。
完成 Java 应用层的 CAS 操作,主要涉及到的 Unsafe 方法调用,具体如下:
(1)获取 Unsafe 实例。
(2)调用 Unsafe 提供的 CAS 方法,这些方法主要封装了底层 CPU 的 CAS 原子操作。
(3)调用 Unsafe 提供的字段偏移量方法,这些方法用于获取对象中的字段(属性)偏移量,此偏移量值需要作为参数提供给 CAS 操作。

2.1 内存管理:Unsafe类提供的直接操纵内存相关的方法

//分配内存指定大小的内存
public native long allocateMemory(long bytes);
//根据给定的内存地址address设置重新分配指定大小的内存
public native long reallocateMemory(long address, long bytes);
//用于释放allocateMemory和reallocateMemory申请的内存
public native void freeMemory(long address);
//将指定对象的给定offset偏移量内存块中的所有字节设置为固定值
public native void setMemory(Object o, long offset, long bytes, byte value);
//设置给定内存地址的值
public native void putAddress(long address, long x);
//获取指定内存地址的值
public native long getAddress(long address);

//设置给定内存地址的long值
public native void putLong(long address, long x);
//获取指定内存地址的long值
public native long getLong(long address);
//设置或获取指定内存的byte值
public native byte  getByte(long address);
public native void  putByte(long address, byte x);
//其他基本数据类型(long,char,float,double,short等)的操作与putByte及getByte相同
.......... 省略代码
//操作系统的内存页大小
public native int pageSize();

2.2 对象实例创建:Unsafe类提供创建对象实例新的途径

//传入一个对象的class并创建该实例对象,但不会调用构造方法
public native Object allocateInstance(Class cls) throws InstantiationException;

2.3 类、实例对象以及变量操作:Unsafe类提供类、实例对象以及变量操纵方法

//获取字段f在实例对象中的偏移量
public native long objectFieldOffset(Field f);
//静态属性的偏移量,用于在对应的Class对象中读写静态属性
public native long staticFieldOffset(Field f);
//返回值就是f.getDeclaringClass()
public native Object staticFieldBase(Field f);


//获得给定对象偏移量上的int值,所谓的偏移量可以简单理解为指针指向该变量的内存地址,
//通过偏移量便可得到该对象的变量,进行各种操作
public native int getInt(Object o, long offset);
//设置给定对象上偏移量的int值
public native void putInt(Object o, long offset, int x);

//获得给定对象偏移量上的引用类型的值
public native Object getObject(Object o, long offset);
//设置给定对象偏移量上的引用类型的值
public native void putObject(Object o, long offset, Object x);
//其他基本数据类型(long,char,byte,float,double)的操作与getInthe及putInt相同

//设置给定对象的int值,使用volatile语义,即设置后立马更新到内存对其他线程可见
public native void  putIntVolatile(Object o, long offset, int x);
//获得给定对象的指定偏移量offset的int值,使用volatile语义,总能获取到最新的int值。
public native int getIntVolatile(Object o, long offset);

//其他基本数据类型(long,char,byte,float,double)的操作与putIntVolatile
//及getIntVolatile相同,引用类型putObjectVolatile也一样。
..........省略代码

//与putIntVolatile一样,但要求被操作字段必须有volatile修饰
public native void putOrderedInt(Object o,long offset,int x);

       Unsafe 类是一个“final”修饰的不允许继承的最终类,Unsafe类是没有对外提供构造函数的,虽然Unsafe类对外提供getUnsafe()方法,但该方法只提供给Bootstrap类加载器使用,普通用户调用将抛出异常,所以我们在下面的Demo中使用反射技术获取了Unsafe实例对象并进行相关操作。

public static Unsafe getUnsafe() {
  Class cc = sun.reflect.Reflection.getCallerClass(2);
  if (cc.getClassLoader() != null)
      throw new SecurityException("Unsafe");
  return theUnsafe;
}
public class UnSafeDemo {
    public  static  void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InstantiationException {
        // 通过反射得到theUnsafe对应的Field对象
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        // 设置该Field为可访问
        field.setAccessible(true);
        // 通过Field得到该Field对应的具体对象,传入null是因为该Field为static的
        Unsafe unsafe = (Unsafe) field.get(null);
        System.out.println(unsafe);

        //通过allocateInstance直接创建对象
        Demo demo = (Demo) unsafe.allocateInstance(Demo.class);

        Class demoClass = demo.getClass();
        Field str = demoClass.getDeclaredField("str");
        Field i = demoClass.getDeclaredField("i");
        Field staticStr = demoClass.getDeclaredField("staticStr");

        //获取实例变量str和i在对象内存中的偏移量并设置值
        unsafe.putInt(demo,unsafe.objectFieldOffset(i),1);
        unsafe.putObject(demo,unsafe.objectFieldOffset(str),"Hello Word!");

        // 返回 User.class
        Object staticField = unsafe.staticFieldBase(staticStr);
        System.out.println("staticField:" + staticStr);

        //获取静态变量staticStr的偏移量staticOffset
        long staticOffset = unsafe.staticFieldOffset(userClass.getDeclaredField("staticStr"));
        //获取静态变量的值
        System.out.println("设置前的Static字段值:"+unsafe.getObject(staticField,staticOffset));
        //设置值
        unsafe.putObject(staticField,staticOffset,"Hello Java!");
        //再次获取静态变量的值
        System.out.println("设置后的Static字段值:"+unsafe.getObject(staticField,staticOffset));
        //调用toString方法
        System.out.println("输出结果:"+demo.toString());

        long data = 1000;
        byte size = 1; //单位字节

        //调用allocateMemory分配内存,并获取内存地址memoryAddress
        long memoryAddress = unsafe.allocateMemory(size);
        //直接往内存写入数据
        unsafe.putAddress(memoryAddress, data);
        //获取指定内存地址的数据
        long addrData = unsafe.getAddress(memoryAddress);
        System.out.println("addrData:"+addrData);

        /**
         * 输出结果:
         sun.misc.Unsafe@0f18aef2
         staticField:class com.demo.Demo
         设置前的Static字段值:Demo_Static
         设置后的Static字段值:Hello Java!
         输出USER:Demo{str='Hello Word!', i='1', staticStr='Hello Java!'}
         addrData:1000
         */

    }
}

class Demo{
    public Demo(){
        System.out.println("我是Demo类的构造函数,我被人调用创建对象实例啦....");
    }
    private String str;
    private int i;
    private static String staticStr = "Demo_Static";

    @Override
    public String toString() {
        return "Demo{" +
            "str = '" + str + '\'' +
            ", i = '" + i +'\'' +
            ", staticStr = " + staticStr +'\'' +
        '}';
    }
}

2.4 数组操作:Unsafe类提供直接获取数组元素内存位置的途径

//获取数组第一个元素的偏移地址
public native int arrayBaseOffset(Class arrayClass);
//数组中一个元素占据的内存空间,arrayBaseOffset与arrayIndexScale配合使用,可定位数组中每个元素在内存中的位置
public native int arrayIndexScale(Class arrayClass);

2.5 CAS相关操作:Unsafe类提供Java中的CAS操作支持

//第一个参数o为给定对象,offset为对象内存的偏移量,通过这个偏移量迅速定位字段并设置或获取该字段的值,
//expected表示期望值,x表示要设置的值,下面3个方法都通过CAS原子指令执行操作。
public final native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);

2.6 JDK8之后新增的基于原有CAS方法的方法

 //1.8新增,给定对象o,根据获取内存偏移量指向的字段,将其增加delta,
 //这是一个CAS操作过程,直到设置成功方能退出循环,返回旧值
 public final int getAndAddInt(Object o, long offset, int delta) {
     int v;
     do {
         //获取内存中最新值
         v = getIntVolatile(o, offset);
       //通过CAS操作
     } while (!compareAndSwapInt(o, offset, v, v + delta));
     return v;
 }

//1.8新增,方法作用同上,只不过这里操作的long类型数据
 public final long getAndAddLong(Object o, long offset, long delta) {
     long v;
     do {
         v = getLongVolatile(o, offset);
     } while (!compareAndSwapLong(o, offset, v, v + delta));
     return v;
 }

 //1.8新增,给定对象o,根据获取内存偏移量对于字段,将其 设置为新值newValue,
 //这是一个CAS操作过程,直到设置成功方能退出循环,返回旧值
 public final int getAndSetInt(Object o, long offset, int newValue) {
     int v;
     do {
         v = getIntVolatile(o, offset);
     } while (!compareAndSwapInt(o, offset, v, newValue));
     return v;
 }

// 1.8新增,同上,操作的是long类型
 public final long getAndSetLong(Object o, long offset, long newValue) {
     long v;
     do {
         v = getLongVolatile(o, offset);
     } while (!compareAndSwapLong(o, offset, v, newValue));
     return v;
 }

 //1.8新增,同上,操作的是引用类型数据
 public final Object getAndSetObject(Object o, long offset, Object newValue) {
     Object v;
     do {
         v = getObjectVolatile(o, offset);
     } while (!compareAndSwapObject(o, offset, v, newValue));
     return v;
 }

3. CAS实现原子操作的三大问题

3.1 ABA问题

       因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化 则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它 的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面 追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。从 Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个 类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是 否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

 public boolean compareAndSet(
            V expectedReference,// 预期引用 
            V newReference, // 更新后的引用 
            int expectedStamp, // 预期标志 
            int newStamp // 更新后的标志 
     )

如何解决这种ABA问题?
解决方法: AtomicReference原子引用。 接下来会详细介绍原子类以及如何使用原子类解决ABA问题。

3.2 循环时间长开销大。

       自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升。pause指令有两个作用:
第 一,它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间 取决于具体实现的版本,在一些处理器上延迟时间是零;
第二,它可以避免在退出循环的时候 因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空(CPU Pipeline Flush),从而 提高CPU的执行效率

3.3 只能保证一个共享变量的原子操作

       当对一个共享变量执行操作时,我们可以使用循 环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子 性,这个时候就可以用锁。还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来 操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始, JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对 象里来进行CAS操作。

4. J.U.C包下的原子操作包Atomic

Atomic 操作翻译成中文,是指一个不可中断的操 作,即使在多个线程一起执行 Atomic 类型操作的时候,一个操作一旦开始,就不会被其他线程中断。所谓Atomic 类,指的是具有原子操作特征的类。

4.1 JUC 并发包中原子类的位置

JUC 并 发 包 中 原 子 类 , 都 存 放 在java.util.concurrent.atomic 类路径下,如图:


JUC 中的 Atomic 原子操作包中的类

根据操作的目标数据类型,可以将 JUC 包中的原子类分为 4 类:
(1)基本原子类,(2)数组原子类, (3)原子引用类型,(4)字段更新原子类。

4.1.1 基本原子类

基本原子类的功能,是通过原子方式更新 Java 基 础类型变量的值。基本原子类主要包括了以下三个:

  • AtomicInteger:整型原子类。
  • AtomicLong:长整型原子类。
  • AtomicBoolean :布尔型原子类。

4.1.2 数组原子类

数组原子类的功能,是通过原子方式更数组里的某个元素的值。数组原子类主要包括了以下三个:

  • AtomicIntegerArray:整型数组原子类。
  • AtomicLongArray:长整型数组原子类。
  • AtomicReferenceArray :引用类型数组原子类。

4.1.3 引用原子类

  • AtomicReference:引用类型原子类。
  • AtomicMarkableReference :带有更新标记位的原子引用类型。
  • AtomicStampedReference :带有更新版本号的原子引用类型。
    AtomicMarkableReference 类将 boolean 标记与引用关联起来,可以解决使用AtomicBoolean进行原子更新时可能出现的 ABA 问题。
    AtomicStampedReference 类将整数值与引用关联起来,可以解决使用 AtomicInteger 进行原子更新时可能出现的 ABA 问题。

4.1.4 字段更新原子类

字段更新原子类主要包括了以下三个:

  • AtomicIntegerFieldUpdater:原子更新整型字段的更新器。
  • AtomicLongFieldUpdater:原子更新长整型字段的更新器。
  • AtomicReferenceFieldUpdater:原子更新引用类型里的字段。

4.2 基础原子类 AtomicInteger

基础原子类 AtomicInteger 常用的方法主要如下:

    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 的使用示例,具体代码如下:

public class AtomicTest {

    public static void main(String[] args) {
        int tempValue = 0;
        // 定义一个整数原子类实例,赋值到变量 i
        AtomicInteger i = new AtomicInteger(0);

        // 取值,然后设置一个新值
        tempValue = i.getAndSet(3);
        // 输出 tempValue:0; i: 3
        System.out.println("tempValue: " + tempValue + "; i : " + i.get());

        // 取值,然后自增
        tempValue = i.getAndIncrement();
        // 输出 tempValue:3; i: 4
        System.out.println("tempValue: " + tempValue + "; i : " + i.get());

        // 取值,然后增加5
        tempValue = i.getAndAdd(5);
        //输出 tempvalue:4; i:9
        System.out.println("tempValue: " + tempValue + "; i : " + i.get());

        //CAS 交换
        boolean flag = i.compareAndSet(9, 100);
        //输出 flag:true; i:100
        System.out.println("flag:" + flag + "; i:" + i.get());
    }
}


4.3 数组原子类 AtomicIntegerArray

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

  • AtomicIntegerArray:整形数组原子类
  • AtomicLongArray:长整形数组原子类
  • AtomicReferenceArray :引用类型数组原子类
    上面三个类提供的方法几乎相同,所以我们这里以 AtomicIntegerArray 为例子来介绍。AtomicIntegerArray 类常用方法,具体如下:
//获取 index=i 位置元素的值
public final int get(int i) 
//返回 index=i 位置的当前的值,并将其设置为新值:newValue
public final int getAndSet(int i, int newValue)
//获取 index=i 位置元素的值,并让该位置的元素自增
public final int getAndIncrement(int i)
//获取 index=i 位置元素的值,并让该位置的元素自减
public final int getAndDecrement(int i) 
//获取 index=i 位置元素的值,并加上预期的值
public final int getAndAdd(int delta) 
//如果输入的数值等于预期值,则以原子方式将 位置 i 的元素值设置为输入值(update)
boolean compareAndSet(int expect, int update) 
//最终将位置 i 的元素设置为 newValue
//lazySet 方法可能导致其他线程在之后的一小段时间内还是可以读到旧的值
public final void lazySet(int i, int newValue)

下面是一个数组原子类 AtomicIntegerArray 的使用示例,具体代码如下:

public class AtomicTest2 {
    public static void main(String[] args) {
        int tempvalue = 0;
        //原始的数组
        int[] array = { 1, 2, 3, 4, 5, 6 };
        //包装为原子数组
        AtomicIntegerArray i = new AtomicIntegerArray(array);
        //获取第 0 个元素,然后设置为 2
        tempvalue = i.getAndSet(0, 2);
        //输出 tempvalue:1; i:[2, 2, 3, 4, 5, 6]
        System.out.println("tempvalue:" + tempvalue + "; i:" + i);
        //获取第 0 个元素,然后自增
        tempvalue = i.getAndIncrement(0);
        //输出 tempvalue:2; i:[3, 2, 3, 4, 5, 6]
        System.out.println("tempvalue:" + tempvalue + "; i:" + i);
        //获取第 0 个元素,然后增加一个 delta 5
        tempvalue = i.getAndAdd(0, 5);
        //输出 tempvalue:3; i:[8, 2, 3, 4, 5, 6]
        System.out.println("tempvalue:" + tempvalue + "; i:" + i);
    }
}

4.4 AtomicInteger 原理

       基础原子类(以 AtomicInteger 为例 )主要通过 CAS 自旋 + volatile 相结合的方案实现,既保障了变量操作的线程安全性,又避免了 synchronized 重量级锁的高开销,使得 Java 程序的执行效率大为提升。
       AtomicInteger 源码中的主要方法,都是通过 CAS 自旋实现的。CAS 自旋的主要操作为:如果一次 CAS 操作失败,则获取最新的 value 值后,再次进行 CAS 操作,直到成功。
另外,AtomicInteger 所包装的内部 value 成员,是一个使用关键字 volatile 修饰的内部成员。关键字 volatile 的原理比较复杂,可以查看深入解析volatile关键字,简单的说,该关键字可以保证任何线程在任何时刻总能拿到该变量的最新值,其目的在于保障变量值的线程可见性。

4.5 对象操作的原子性之引用类型原子类

引用类型原子类包括以下三种:

  • AtomicReference:基础的引用原子类
  • AtomicStampedReference:带印戳的引用原子类
  • AtomicMarkableReference:带修改标志的引用原子类
    上面三种提供的方法几乎相同,所以我们这里以 AtomicReference 为例子来介绍。
public class AtomicReferenceTest {
    public static void main(String[] args) {
        //包装的原子对象
        AtomicReference userRef = new AtomicReference();
        //待包装的 User 对象
        User user = new User("1", "张三");
        //为原子对象设置值
        userRef.set(user);
        System.out.println(" userRef is:" + userRef.get());
        //要使用 CAS 替换的 User 对象
        User updateUser = new User("2", "李四");
        //使用 CAS 替换
        boolean success = userRef.compareAndSet(user, updateUser);
        System.out.println(" cas result is:" + success);
        System.out.println(" after cas,userRef is:" + userRef.get());
    }
}

class User implements Serializable {
    //用户 ID
    String uid;
    //昵称
    String nickName;
    //年龄
    public volatile int age;

    public User(String uid, String nickName) {
        this.uid = uid;
        this.nickName = nickName;
    }

    @Override
    public String toString()
    {
        return "User{" +
                "uid='" + getUid() + '\'' +
                ", nickName='" + getNickName() + '\''+
                '}';
    }

    public String getUid() {
        return uid;
    }

    public void setUid(String uid) {
        this.uid = uid;
    }

    public String getNickName() {
        return nickName;
    }

    public void setNickName(String nickName) {
        this.nickName = nickName;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}


输出如下:

   userRef is:User{uid='1', nickName='张三'}
   cas result is:true
   after cas,userRef is:User{uid='2', nickName='李四'}

       以上代码首先创建了一个 User 对象,然后把 User 对象包装到一个 AtomicReference 类型的引用 userRef 中,如果要修改 userRef 的包装值,需要调用 compareAndSet 方法才能完成。该方法就是通过 CAS 操作 userRef,从而保证操作的原子性。
需要注意的是:使用原子引用类型 AtomicReference 包装了 User 对象之后,只能保障 User 引用的原子操作,对被包装的 User 对象的字段值修改时不能保证原子性的,这点要切记。

4.6 对象操作的原子性之属性更新原子类

如果需要保障对象某个字段(或者属性)更新操作的原子性,需要用到属性更新原子类。属性更新原子类有以下三个:

  • AtomicIntegerFieldUpdater:保障整形字段的更新操作的原子性。
  • AtomicLongFieldUpdater:保障长整形字段的更新操作的原子性。
  • AtomicReferenceFieldUpdater:保障引用字段的更新操作的原子性。
    以 AtomicIntegerFieldUpdater 为例来介绍。使用属性更新原子类去保障属性安全更新的流程,大致需要两步:
    第一步,更新的对象属性必须使用 public volatile 修饰符。
    第二步,因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 newUpdater( )创建一个更新器,并且需要设置想要更新的类和属性。
public class AtomicIntegerFieldUpdaterTest {
    public static void main(String[] args) {
        //使用静态方法 newUpdater( )创建一个更新器 updater
        AtomicIntegerFieldUpdater updater=
                AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
        User user = new User("1", "张三");
        //使用属性更新器的 getAndIncrement、getAndAdd 增加 user 的 age 值
        System.out.println(updater.getAndIncrement(user));// 1
        System.out.println(updater.getAndAdd(user, 100));// 101
        //使用属性更新器的 get 获取 user 的 age 值
        System.out.println(updater.get(user));// 101
    }
}

User类使用的和AtomicReference使用的User是同一个。
运行结果如下:

  0
  1
  101

5. 结合原子类解决ABA问题

5.1 使用 AtomicStampedReference 解决 ABA 问题

       参考乐观锁的版本号,JDK 的提供了一个类似 AtomicStampedReference 类来解决 ABA 问题。AtomicStampReference 在 CAS 的基础上增加了一个 Stamp 印戳(或标记),使用这个印戳可以用来觉察数据是否发生变化,给数据带上了一种实效性的检验。
       AtomicStampReference 的 compareAndSet 方法首先检查当前的对象引用值是否等于预期引用,并且当前印戳(Stamp)标志是否等于预期标志,如果全部相等,则以原子方式将引用值和印戳(Stamp标志的值更新为给定的更新值。
AtomicStampReference 的构造器有两个参数,具体如下:

  //构造函数,V 表示要引用的原始数据,initialStamp 表示最初的版本印戳(版本号)
AtomicStampedReference(V initialRef, int initialStamp)

AtomicStampReference 的常用的几个方法如下:

//获取被封装的数据
public V getRerference();
//获取被封装的数据的版本印戳
public int getStamp();

AtomicStampedReference 的 CAS 操作的定义如下:

public boolean compareAndSet(
 V expectedReference,//预期引用值
 V newReference, //更新后的引用值
 int expectedStamp, //预期印戳(Stamp)标志值
 int newStamp) //更新后的印戳(Stamp)标志值

       compareAndSet 方法的第一个参数是原来的 CAS 中原来参数,第二个参数是要替换后的新参数,第三个参数是原来 CAS 数据旧的版本号,第四个参数表示替换后的新参数版本号。进行 CAS 操作时,只有当前引用值等于预期引用值,并且当前印戳值等于预期印戳值,则以原子方式将引用值和印戳(Stamp)值更新为给定的更新值。
       下面是一个简单的 AtomicStampedReference 使用示例,通过两个线程分别带上印戳更新同一个 atomicStampedRef 实例的值,第一个线程会更新成功,而第二个线程更新失败,具体代码如下:

public class AtomicStampedReferenceTest{
    // 定义原子计数器,初始值 = 100
    private static AtomicInteger atomicI = new AtomicInteger(100);
    // 定义AtomicStampedReference:初始化时需要传入一个初始值和初始时间
    private static AtomicStampedReference asRef = new AtomicStampedReference(100, 0);

    /**
     * 未使用AtomicStampedReference线程组:TA TB
     */
    private static Thread TA = new Thread(() -> {
        System.err.println("未使用AtomicStampedReference线程组:[TA TB] >>>>");
        // 更新值为101
        boolean flag = atomicI.compareAndSet(100, 101);
        System.out.println("线程TA:100 -> 101.... flag:" + flag + ",atomicINewValue:" + atomicI.get());
        // 更新值为100
        flag = atomicI.compareAndSet(101, 100);
        System.out.println("线程TA:101 -> 100.... flag:" + flag + ",atomicINewValue:" + atomicI.get());
    });
    private static Thread TB = new Thread(() -> {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        boolean flag = atomicI.compareAndSet(100, 888);
        System.out.println("线程TB:100 -> 888.... flag:" + flag + ",atomicINewValue:" + atomicI.get() + "\n\n");
    });

    /**
     *  使用AtomicStampedReference线程组:T1 T2
     */
    private static Thread T1 = new Thread(() -> {
        System.err.println("使用AtomicStampedReference线程组:[T1 T2] >>>>");
        // 更新值为101
        boolean flag = asRef.compareAndSet(100, 101, asRef.getStamp(), asRef.getStamp() + 1);
        System.out.println("线程T1:100 -> 101.... flag:" + flag + ".... asRefNewValue:" + asRef.getReference() + ".... 当前Time:" + asRef.getStamp());
        // 更新值为100
        flag = asRef.compareAndSet(101, 100, asRef.getStamp(), asRef.getStamp() + 1);
        System.out.println("线程T1:101 -> 100.... flag:" + flag + ".... asRefNewValue:" + asRef.getReference() + ".... 当前Time:" + asRef.getStamp());
    });
    private static Thread T2 = new Thread(() -> {
        int time = asRef.getStamp();
        System.out.println("线程休眠前Time值:" + time);
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        boolean flag = asRef.compareAndSet(100, 888, time, time + 1);

        System.out.println("线程T2:100 -> 888.... flag:" + flag + ".... asRefNewValue:" + asRef.getReference() + ".... 当前Time:" + asRef.getStamp());
    });

    public static void main(String[] args) throws InterruptedException {
        TA.start();
        TB.start();
        TA.join();
        TB.join();

        T1.start();
        T2.start();
    }
}

/**
 * 未使用AtomicStampedReference线程组:[TA TB] >>>>
 * 线程TA:100 -> 101.... flag:true,atomicINewValue:101
 * 线程TA:101 -> 100.... flag:true,atomicINewValue:100
 * 线程TB:100 -> 888.... flag:true,atomicINewValue:888
 *
 *
 * 使用AtomicStampedReference线程组:[T1 T2] >>>>
 * 线程休眠前Time值:0
 * 线程T1:100 -> 101.... flag:true.... asRefNewValue:101.... 当前Time:1
 * 线程T1:101 -> 100.... flag:true.... asRefNewValue:100.... 当前Time:2
 * 线程T2:100 -> 888.... flag:false.... asRefNewValue:100.... 当前Time:2
 */

       我们观察如上Demo中AtomicInteger与AtomicStampedReference的测试结果可以得知,AtomicStampedReference确实能够解决我们在之前阐述的ABA问题,那么AtomicStampedReference究竟是如何ABA问题的呢?我们接下来再看看它的内部实现:

public class AtomicStampedReference {
    // 通过Pair内部类存储数据和时间戳
    private static class Pair {
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static  Pair of(T reference, int stamp) {
            return new Pair(reference, stamp);
        }
    }
    // 存储数值和时间的内部类
    private volatile Pair pair;

    // 构造方法:初始化时需传入初始值和时间初始值
    public AtomicStampedReference(V initialRef, int initialStamp) {
        pair = Pair.of(initialRef, initialStamp);
    }
}

compareAndSet方法源码实现:

public boolean compareAndSet(V expectedReference,
                         V newReference,
                         int expectedStamp,
                         int newStamp) {
    Pair current = pair;
    return
        expectedReference == current.reference &&
        expectedStamp == current.stamp &&
        ((newReference == current.reference &&
          newStamp == current.stamp) ||
         casPair(current, Pair.of(newReference, newStamp)));
}

// 最终实现调用Unfase类中的compareAndSwapObject()方法
private boolean casPair(Pair cmp, Pair val) {
    return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}

5.2 使用 AtomicMarkableReference 解决 ABA 问题

       AtomicMarkableReference 是 AtomicStampedReference 的简化版,不关心修改过几次,仅仅关心是否修改过。因此其标记属性 mark 是 boolean 类型,而不是数字类型,标记属性 mark 仅记录值是否有过修改。
AtomicMarkableReference 适用只要知道对象是否有被修改过,而不适用于对象被反复修改的场景。

public class AtomicMarkableReferenceTest {
    static AtomicMarkableReference amRef = new AtomicMarkableReference(100, false);

    private static Thread t1 = new Thread(() -> {
        boolean mark = amRef.isMarked();
        System.out.println("线程T1:修改前标志 Mrak:" + mark + "....");
        // 将值更新为200
        System.out.println("线程T1:100 --> 200.... 修改后返回值 Result:" + amRef.compareAndSet(amRef.getReference(), 200, mark, !mark));
    });

    private static Thread t2 = new Thread(() -> {
        boolean mark = amRef.isMarked();
        System.out.println("线程T2:修改前标志 Mrak:" + mark + "....");
        // 将值更新回100
        System.out.println("线程T2:200 --> 100.... 修改后返回值 Result:" + amRef.compareAndSet(amRef.getReference(), 100, mark, !mark));
    });

    private static Thread t3 = new Thread(() -> {
        boolean mark = amRef.isMarked();
        System.out.println("线程T3:休眠前标志 Mrak:" + mark + "....");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        boolean flag = amRef.compareAndSet(100, 500, mark, !mark);
        System.out.println("线程T3: 100 --> 500.... flag:" + flag + ",newValue:" + amRef.getReference());
    });

    public static void main(String[] args) throws InterruptedException {
        t1.start();
        t1.join();
        t2.start();
        t2.join();
        t3.start();

        /**
         * 输出结果如下:
         *    线程T1:修改前标志 Mrak:false....
         *    线程T1:100 --> 200.... 修改后返回值 Result:true
         *    线程T2:修改前标志 Mrak:true....
         *    线程T2:200 --> 100.... 修改后返回值 Result:true
         *    线程T3:休眠前标志 Mrak:false....
         *    线程T3: 100 --> 500.... flag:true,newValue:500
         */

         /* t3线程执行完成后结果还是成功更新为500,代表t1、t2
          线程所做的修改操作对于t3线程来说还是不可见的 */
    }
}

6. CAS 操作在 JDK 中的应用

       CAS 在 java.util.concurrent.atomic 包中的原子类、Java AQS 以及显示锁、CurrentHashMap 等重要并发容器类的实现上,都有非常广泛的应用。
       在 java.util.concurrent.atomic 包的原子类如 AtomicXXX 中,都使用了 CAS 保障对数字成员进行操作的原子性。
       java.util.concurrent 的大多数类(包括显示锁、并发容器)都基于 AQS 和 AtomicXXX 实现,而 AQS 通过 CAS 保障其内部双向队列头部、尾部操作的原子性。

参考来源:
《Java并发编程的艺术》
《深入理解Java虚拟机》
《Java高并发程序设计》
《Java并发编程实战》
《Java并发编程之美》

你可能感兴趣的:(Java并发编程之CAS,原子包Atomic)