Bootstrap

Java原子操作与原子类

1. 概述

  • JVM中的CAS操作,Compare And Swap,它依靠处理器的CMPXCHG指令实现原子操作
  • 执行CMPXCHG指令,需要三个操作数:内存地址 V、旧的预期值 A 和新值 B。
  • 执行操作时,只有当内存 地址V中 的值等于 A,才将内存地址 V中 的值更新为 B。

1.1 什么是原子操作

  • 原子操作的书面定义:不可中断的一个或一系列操作
  • 这样的定义是晦涩难懂的,可以结合银行转账的例子来说明
    • lucy向Bob转账100元,预期的操作步骤:lucy的账户减少100元,Bob的账户增加100元
    • 银行系统不是很智能,lucy的账户减少100元后,由于某些问题系统服务崩溃
    • 即使系统恢复了,既没有将Bob的账户增加100元,完成转账操作;也没有恢复lucy账户,告知转账失败
  • 可以说,银行转账的过程应该是一个整体,其顺序不能被打乱,也不能被突然中止;要么都执行成功,要么都执行失败或者不执行
  • 因此,原子操作可以这样理解:
    • 计算机系统中一个流程,可能是一个步骤,也可能是多个步骤
    • 流程的执行顺序不可以打乱,也不可以被切割而只执行其中的一部分
    • 应该将整个流程视为不可以改变、分割的整体,就像一个原子一样

1.2 原子操作的重要性

  • 需求
    • 地铁一共有5个闸机口,设计一个程序统计总的进站人数
    • 为了方便模拟进站计数,假设每个闸机口固定为1万人
  • 有人会说,这样一规定直接可以口算啊。
  • 有时候吧,人和机器就是有差别的,让机器实现计数功能,都不一定能准确呢 😂

半罐水的菜鸟的实现

  • 这个简单,开5个线程,每个线程都让计数器的值加一1万次

  • 使用volatile,保证上一个线程修改计数器的值后,下一个线程立马可见

    public class Counter {
        private static volatile int i;
        private static final int THREAD_COUNT = 5;
        private static final CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT);
    
        public static void count() {
            i++;
        }
    
        public static void main(String[] args) throws InterruptedException {
            Thread[] threads = new Thread[THREAD_COUNT];
            // 创建计数线程并开始计数
            for (int i = 0; i < THREAD_COUNT; i++) {
                threads[i] = new Thread(() -> {
                    for (int j = 0; j < 10000; j++) {
                        count();
                    }
                    countDownLatch.countDown();
                });
                threads[i].start();
            }
            // 完成计数后,打印值
            countDownLatch.await();
            System.out.println("今日总进站人数: " + i);
        }
    }
    
  • 一执行代码,傻眼了:怎么最后结果不是5万?而且差的也太远了吧 😂
    在这里插入图片描述

  • 原因: volatile只能多线程下的可见性,不能保证非原子操作的线程安全

  • i++操作不是一个原子操作,导致putstatic放回去的可能是较小值

  • 情况一: 重复计数问题,导致putstatic放回去的是较小值

    时间线程1线程2结果
    t1getstaticgetstatic线程1和2,都获取到i的最新值 20
    t2inconst_1
    iadd
    inconst_1
    iadd
    线程1和2,各自的工作内存中i的值均加1
    t3putstatic线程2将自己工作内存中的i = 21写回主内存
    t4putstatic线程1自己工作内存中的i = 21写回主内存
  • 还可能有更极端的情况:

    • 线程1执行getstatic获取到i的最新值20后,由于CPU时间耗尽被挂起
    • 之后其他线程正常工作,已经通过putstatici更新为了24
    • 线程1再次获取大CPU时间片,仍然基于旧的20进行加一操作;执行putstatic将21写回到内存,一下子就将之前的计数工作打回原形
      在这里插入图片描述

加锁

  • 通过synchronized关键给count() 方法加锁,保证同一时刻只有一个线程将计数器加一
  • 计数正确,但是同一时刻只能有一个线程访问count() 方法,计数效率十分低下
    在这里插入图片描述
  • 就像过闸机时,只有一个工作人员在那里数数,实际是串行计数

通过原子类实现线程安全的计数器

  • 既然i++操作不是原子操作,可以考虑使用Java的原子类,进行具有原子性的加一操作

    • 循环的CAS操作,每次加一前都先获取当前值
    • 在当前线程执行CAS加一操作前,若没有其他线程更新值,则CAS操作成功;否则,进行下一次CAS加一操作的尝试直到成功
      public class Counter {
          private static final int THREAD_COUNT = 5;
          private static final CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT);
          private AtomicInteger i = new AtomicInteger();
      
          public void count() {
              // 通过循环的CAS实现原子加一
              for (; ; ) {
                  int current = i.get();
                  if (i.compareAndSet(current, ++current)) {
                      break;
                  }
              }
          }
      
          public int getValue(){
              return i.get();
          }
      
          public static void main(String[] args) throws InterruptedException {
              Thread[] threads = new Thread[THREAD_COUNT];
              Counter counter = new Counter();
              // 创建计数线程并开始计数
              for (int i = 0; i < THREAD_COUNT; i++) {
                  threads[i] = new Thread(() -> {
                      for (int j = 0; j < 10000; j++) {
                          counter.count();
                      }
                      countDownLatch.countDown();
                  });
                  threads[i].start();
              }
              // 完成计数后,打印值
              countDownLatch.await();
              System.out.println("今日总进站人数: " + counter.getValue());
          }
      }
      
  • 多线程下,基于原子类、通过循环的CAS加一操作成功计数:

2. CAS实现原子操作的三个问题

2.1 ABA问题

  • 基于CAS操作,考虑银行系统转账场景
    • 小明账户余额100元,通过手机银行向小王转账50元
    • 由于系统内部错误,出现两个线程同时进行转账操作
    • 线程1获取账户余额100作为期望值,比较后发现符合,于是将账户余额更新为50元(转账成功)
    • 线程2同一时间,也获取账户余额100作为期望值,然后block了
    • 之后,小王还款50元,账户余额变成100元
    • 线程2恢复,比较后发现符合预期,于是再次执行转账操作、小明的户余额又变成了50元
  • 这就是所谓的ABA问题:
    • 一个数的值原来是A —> 更新成B —> 更新回A
    • 这时使用CAS进行检查是发现它没有发生变化,于是成功更新
    • 实际上,这个数已经变化多次,只是最后变回了A而已
  • ABA问题,可以通过添加版本号解决:A —> B —> A添加版本号后,1A —> 2B —> 3A
  • 这时,发现不是期望的1A,而是3A,因此不会进行CAS更新

JDK提供的解决方案 —— AtomicStampedReference原子类

  • JDK的java.util.concurrent.atomic包,简称Atomic包,里面提供了12中支持原子操作的实用类
  • 其中,AtomicStampedReference类可以原子更新带有版本号的引用类型
  • 类中的compareAndSet() 方法,要求预期引用等于当前引用、预期版本号等于当前版本号,然后以原子的方式更新引用和版本号
    • 完成版本号和引用校验后,调用casPair() 方法进行原子更新
    • casPair() 方法最终将调用Unsafe类的native方法compareAndSwapObject() 进行原子更新
      public boolean compareAndSet(V   expectedReference, // 预期引用
                                   V   newReference, // 更新后的引用
                                   int expectedStamp,  // 预期版本号
                                   int newStamp) { // 更新后的版本号
          Pair<V> current = pair;
          return
              expectedReference == current.reference &&
              expectedStamp == current.stamp &&
              ((newReference == current.reference &&
                newStamp == current.stamp) ||
               casPair(current, Pair.of(newReference, newStamp)));
      }
      
      private boolean casPair(Pair<V> cmp, Pair<V> val) {
          return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
      }
      
  • 自己的猜测CMPXCHG指令的实现:
    • 更新时会锁定内存、获取内存中的值
    • 如果发现内存中的值与预期值不一致,则不进行更新并返回false
    • 如果内存中的值与预期值一致,则进行更新并会返回true
    • 也就是说,casPair() 方法只是尝试进行一次原子更新,并不保证此次更新一定能成功
  • 这也是为啥实际调用compareAndSet() 时,需要以死循环的方式(自旋CAS)进行调用

2.2 循环时间长、开销大

  • 自旋CAS和自旋锁一样,如果长时间不成功,会给CPU带来较大的开销
  • 针对这个问题,貌似没有给出解决方案,就是说:如果JVM能支持操作系统的pause指令,那么效率会有一定的提升
    ① 可以延迟流水线执行指令,是CPU不会消耗过多的执行资源
    ② 可以避免在退出循环的时候因为内存顺序冲突,导致CPU流水线被清空,从而提高CPU的执行效率

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

  • 之前的地铁乘车流量计数器,只能计算进站人数,如果要求同时计算男女生人数

  • 照葫芦画瓢,在定义两个AtomicInteger类型的变量,统计男生或女生人数

    public void count() {
        // CAS计算进站总人数
        for (; ; ) {
            int current = total.get();
            if (total.compareAndSet(current, ++current)) {
                break;
            }
        }
        // CAS计算进站男生人数
        if (men) {
    	    for (; ; ) {
    	        int current = men.get();
    	        if (men.compareAndSet(current, ++current)) {
    	            break;
    	        }
    	    }
        }
        ...
    }
    
  • 上面的代码存在问题,线程在完成进站总人数的更新后,如果线程中断未完成男女生的计数,会导致最终统计的数值存在问题

  • 因此,循环CAS操作只能保证对一个共享变量的原子操作,要想对多个变量执行原子操作:

    • 方法一:加锁,对多个共享变量的操作放到同一个临界区,保证操作的原子性
    • 方法二:取巧,将多个变量合为一个支持原子操作的变量,例如,ij组合成k = i * j
    • 方法三:使用AtomicReference类保证引用类型的原子性,其中多个共享变量作为引用类型的成员变量

3. Atomic包中的原子操作类

  • JDK的Atomic包,提供了很多实用的原子操作类
  • 包括:原子更新基本数据类型、原子更新数组、原子更新引用类型、原子更新字段

3.1 Unsafe类

  • 这些原子操作类,几乎都是基于Unsafe类实现CAS操作
  • Unsafe类中实现CAS操作的方法都是native方法:支持对象、int和long三种类型的原子操作
    public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
    
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
    
    public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
    

3.2 原子更新基本数据类型

  • Java中的基本数据类型有:boolean、byte、short、char、int、long、float、double
  • Atomic包提供了针对boolean、int和long三种基本数据类型的原子操作类: AtomicBooleanAtomicIntegerAtomicLong
  • 下面将基于AtomicInteger进行讲解

其成员变量如下:

  • unsafe:Unsafe类的实例,是实现原子操作的基础
  • value:就是AtomicInteger对应的数值,使用volatile修饰,表示多线程访问时的可见性(后续会详细说)
  • valueOffset:value字段的内存地址偏移量,可以根据偏移量计算绝对地址,后续更新数值需要使用该内存地址
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;
    
    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    
    private volatile int value;
    

compareAndSet() 方法:

  • 尝试进行一次CAS操作,将value更新从预期值(expect)更新为指定值(update);成功返回true,否则返回false
  • 实际调用Unsafe类的compareAndSwapInt()方法,尝试进行一次CAS操作
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
    
  • 注意: 这里只是尝试进行一次CAS操作,若想要保证一定会成功,则需要进行循环的CAS操作,
  • 就像之前的count代码一样:
    • 不停获取计数器的最新值作为预期值,然后尝试通过CAS操作将其加1

    • 最新值,由AtomicInteger的value字段前的 volatile关键字保证

      public void count() {
          for (; ; ) {
              int current = i.get();
              if (i.compareAndSet(current, ++current)) {
                  break;
              }
          }
      }
      

一些实用方法

  • compareAndSet() 方法,使用时还需要自己通过循环保证CAS操作的成功性,比较麻烦

  • AtomicInteger中也提供了很多实用的方法,这些方法自己保证CAS操作的成功性

  • 这些实用方法分为两类:返回旧值、返回新值

  • 返回旧值

    public final int getAndIncrement()  // 原子自增
    public final int getAndDecrement() // 原子自减
    public final int getAndAdd(int delta)  // 原子增加指定值
    public final int getAndSet(int newValue) // 原子更新为指定值
    
  • 返回新值

    public final int incrementAndGet() 
    public final int decrementAndGet() 
    public final int addAndGet(int delta) 
    
  • 这些实用方法,最终都调用了Unsafe类的getAndAddInt()方法

    public final int getAndAdd(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }
    
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    
  • getAndAddInt() 方法内部,通过循环CAS操作实现原子更新,这也是这些方法为什么更实用的原因

    • var1 是当前对象,var2 是value在对象中的offset,通过 var1 和 var2 可以计算出value的真实内存

    • var4 是变化量, var5 是获取的value最新值作为预期值,var5 + var4就是新值

    • 然后调用compareAndSwapInt()尝试将value从 var5 原子更新为var5 + var4

      public final int getAndAddInt(Object var1, long var2, int var4) {
          int var5;
          do {
              var5 = this.getIntVolatile(var1, var2); // 每次都获取内存中的最新值
          } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
      
          return var5;
      }
      

如何实现其他基本数据类型的原子更新?

  • Atomic包中,只提供了对boolean、int、long三种类型对应的原子操作类
  • 那其他的基本数据类型如何实现原子操作呢?
  • AtomicInteger实现原子操作最终使用Unsafe类的compareAndSwapInt()方法
  • 回到Unsafe类,之前了解过它有三个实现原子操作的方法,分别针对对象、int和long
  • 因此,可以猜测AtomicLong最终使用Unsafe类的compareAndSwapLong()方法实现原子操作
  • 在Unsafe类中,没有针对boolean类型的类似方法,AtomicBoolean的原子操作实际依靠compareAndSwapInt()方法实现
  • 将Boolean类型转为int的1、0,表示true、false,从而可以使用compareAndSwapLong()方法实现CAS操作
    public final boolean compareAndSet(boolean expect, boolean update) {
        int e = expect ? 1 : 0;
        int u = update ? 1 : 0;
        return unsafe.compareAndSwapInt(this, valueOffset, e, u);
    }
    
  • 因此,其他未提供对应原子类的基本数据类型,也可以考虑使用类似的替换方法

3.3 原子更新数组

  • Atomic包还提供以下3个类实现数组中元素的原子更新
    • AtomicIntegerArray:原子更新整型数组中的元素
    • AtomicLongArray:原子更新长整型数组中的元素
    • AtomicReferenceArray :原子更新引用类型数组中的元素
  • 下面将以 AtomicLongArray为例,讲解相关的实现

成员变量

  • unsafe:还是依靠Unsafe类实现数组元素的原子更新
  • base:数组元素的基准offset
  • shift:位移运算中的位移数,实际为3,其作用后续分析
  • array: 实际存储long元素的数组
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final int base = unsafe.arrayBaseOffset(long[].class);
    private static final int shift;
    private final long[] array;
    
    static {
        int scale = unsafe.arrayIndexScale(long[].class);
        if ((scale & (scale - 1)) != 0)
            throw new Error("data type scale not a power of two");
        shift = 31 - Integer.numberOfLeadingZeros(scale);
    }
    
  • array为final类型,说明说明AtomicLongArray定义的long数组是不可变
    • 这里的不可变,不是指数组中元素不可变,而是指数组这个外壳不可变
    • 也就是说,一旦初始化后,不会再重新分配新的内存
    • 这样的话,数组的base也不会变

compareAndSet()方法

  • AtomicLongArray的compareAndSet() 方法,需要提供元素的index、期望值和新值
  • 实现CAS操作的调用链为 compareAndSet() → \rightarrow compareAndSetRaw() → \rightarrow unsafe.compareAndSwapLong()
    public final boolean compareAndSet(int i, long expect, long update) {
        return compareAndSetRaw(checkedByteOffset(i), expect, update);
    }
    
    private boolean compareAndSetRaw(long offset, long expect, long update) {
        return unsafe.compareAndSwapLong(array, offset, expect, update);
    }
    
  • 其中,compareAndSetRaw() 方法的offset:是通过下标i计算出的元素相对起始位置的offset
    • byteOffset() 方法实现了offset的计算:
      (1)i << shift,即 i << 3,也就是乘以 2 3 = 8 2^3 = 8 23=8
      (2)一个long类型的字节数为8,则 i << 3计算出来的就是索引为i的元素相对起始元素的offset
      (3)相对起始元素的offset加上基准offset,就可以确定索引为i的元素的内存地址
      private long checkedByteOffset(int i) {
          if (i < 0 || i >= array.length)
              throw new IndexOutOfBoundsException("index " + i);
      
          return byteOffset(i);
      }
      
      private static long byteOffset(int i) {
          return ((long) i << shift) + base; // 计算offset的关键
      }
      

注意:

  • 同AtomicInteger的compareAndSet()方法一样,AtomicLongArray的compareAndSet() 方法也只是尝试进行一次数组元素的更新,可能成功,也可能失败
  • 使用时,可以通过循环保证CAS操作的成功

实用方法

  • 同样的,AtomicLongArray也提供了一些保证成功原子更新数组元素的实用方法,而无需使用时自动添加循环CAS的有关代码

  • 获取新值的更新方法,其中i表述元素的索引

    public long addAndGet(int i, long delta)
    public final long decrementAndGet(int i)
    public final long incrementAndGet(int i) 
    public final int getAndSet(int i, int newValue) 
    
  • 获取旧值的更新方法

    public final long getAndAdd(int i, long delta)
    public final long getAndDecrement(int i) 
    public final long getAndIncrement(int i)
    

3.4 原子更新引用类型

  • 上面提到过,解决CAS操作的ABA问题、只能原子更新一个共享变量的问题,都提到了使用AtomicXxxReference原子类实现
  • 其实质都是将多个变量看做一个整体,也就是引用类型,从而对其进行更新
  • Atomic包提供了以下3个原子更新引用类型的类:
    • AtomicReference:原子更新引用类型,也就是以原子的方式更新一个对象

      public final boolean compareAndSet(V expect, V update) {
          return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
      }
      
    • AtomicStampedReference:原子更新带有版本号的引用类型,只有当前版本号、引用类型与预期版本号、引用类型一致时,才会更新成的版本号与预期类型

      public boolean compareAndSet(V   expectedReference,
                                   V   newReference,
                                   int expectedStamp,
                                   int newStamp) {
          Pair<V> current = pair;
          return
              expectedReference == current.reference &&
              expectedStamp == current.stamp &&
              ((newReference == current.reference &&
                newStamp == current.stamp) ||
               casPair(current, Pair.of(newReference, newStamp)));
      }
      
    • AtomicMarkableReference:原子更新带有标记的引用类型,只有当前标记、引用类型与预期标记、引用类型一致时,才会更新成的标记与预期类型

      public boolean compareAndSet(V       expectedReference,
                                   V       newReference,
                                   boolean expectedMark,
                                   boolean newMark) {
          Pair<V> current = pair;
          return
              expectedReference == current.reference &&
              expectedMark == current.mark &&
              ((newReference == current.reference &&
                newMark == current.mark) ||
               casPair(current, Pair.of(newReference, newMark)));
      }
      
  • 原子更新引用类型,实际调用的都是unsafe类的compareAndSwapObject() 方法
  • 下面以AtomicStampedReference来看看原子更新应用类型的实现

成员变量

  • 三个成员变量
    • UNSAFE:Unsafe类的实例对象,是实现原子更新的基础
    • pair:将版本号和引用类型合成一个新的类,因此可以同时保证版本号和引用类型的原子更新
    • pairOffset:计算pair字段在类中的偏移量,从而可以计算出pair字段真实的内存地址
      private volatile Pair<V> pair;
      private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe();
      private static final long pairOffset =
          objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class);
      private static class Pair<T> {
          final T reference;
          final int stamp;
          private Pair(T reference, int stamp) {
              this.reference = reference;
              this.stamp = stamp;
          }
          static <T> Pair<T> of(T reference, int stamp) {
              return new Pair<T>(reference, stamp);
          }
      }
      

compareAndSet()方法

  • 代码如下,实质调用casPair方法完成pair(版本号和引用类型)的原子更新

    private boolean casPair(Pair<V> cmp, Pair<V> val) {
        return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
    }
    
  • 除了同时原子更新版本号和引用类型,还可以单独原子更新版本号

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

关于实用方法

  • 原子更新应用类型的几个类,没有像之前的类一样,提供一些实用方法
  • 想想也是有道理的,就拿AtomicStampedReference类来说,就算每次都将版本号递增,但是引用类型中的字段呢?
  • 这些字段各种类型都有,值的更新是无法预料的
  • 而之前的int或long的原子操作一样,很单纯地就只有加减运算更新value,因此能提供一些实用方法

3.5 原子更新字段

  • Atomic包都提供了对引用类型的整体更新,同样,也提供了原子更新对象中某个字段的类

    • AtomicIntegerFieldUpdater:原子更新对象中int类型的字段

    • AtomicLongFieldUpdater:原子更新对象中long类型的字段

    • AtomicReferenceFieldUpdater:原子更新引用类型的字段,这个是顶配。

      public abstract class AtomicIntegerFieldUpdater<T>
      public abstract class AtomicLongFieldUpdater<T>
      public abstract class AtomicReferenceFieldUpdater<T,V> 
      
  • 所有的基本数据类型都有对应的包装类,因此它可以更新基本数据类型和引用类型,涵盖几乎所有的更新需求

  • 下面将以AtomicIntegerFieldUpdater为例,看看其如何实现字段的原子更新

如何创建一个更新器?

  • 从类的定义可以看出,原子更新字段的相关类实际都是抽象类,因此无法通过new AtomicIntegerFieldUpdater()创建一个更新器

  • 原子更新字段的这些抽象类,都在自己内部定义了一个静态实现类。

  • AtomicIntegerFieldUpdater的实现类定义如下

    private static final class AtomicIntegerFieldUpdaterImpl<T>
            extends AtomicIntegerFieldUpdater<T>
    
  • 但是,静态实现类是private的,外部类也无法调用该实现类的构造方法创建一个更新器

  • 因此,在抽象类中提供了一个工具方法newUpdater()以创建对应的更新器

    • tclass:字段所在的类
    • fieldName:字段名
      public static <U> AtomicIntegerFieldUpdater<U> newUpdater(Class<U> tclass,
                                                                String fieldName) {
          return new AtomicIntegerFieldUpdaterImpl<U>
              (tclass, fieldName, Reflection.getCallerClass());
      }
      
  • 留下一个疑问: 为啥不是直接传入一个引用类型(实例对象),而是传入所在类的Class对象?

一点题外话

  • 关于字段名,最开始自己的设想是,知道Class对象、字段名、字段类型(int),那很容易通过反射更新该字段
  • 后来看了具体的实现,发现自己忽略了CAS操作的实质:预期值与当前内存地址中的值做比较,一致则将内存地址中的值更新为新值
  • 整个操作时针对内存的直接读写操作,根本无需使用反射

实现类的成员变量和构造函数

  • 实现类的成员变量和构造函数如下
    • 不出意外,成员变量中存在Unsafe类实例U、字段在对象中的offset、对应类的Class对象tclass
    • 关于cclass,按照注释和相关资料的介绍:要么为调用者类的Class对象,要么为tclass (自己目前不是很理解)
    • 构造函数为了初始化这些成员变量,做了很多事情:
      • 通过反射获取字段的Field对象以及修饰符,要求是可访问的、volatile的int类型字段
      • 获取所在类和调用者的类加载器
      • 还有访问权限、int类型、volatile修饰符的校验,字段在对象中的offset的计算、cclass的更新等
      private static final sun.misc.Unsafe U = sun.misc.Unsafe.getUnsafe();
      private final long offset;
      private final Class<?> cclass;
      private final Class<T> tclass;
      
      AtomicIntegerFieldUpdaterImpl(final Class<T> tclass,
                                   final String fieldName,
                                   final Class<?> caller) {
         // 通过反射获取字段的Field对象以及修饰符
         final Field field;
         final int modifiers;
         try {
             field = AccessController.doPrivileged(
                 new PrivilegedExceptionAction<Field>() {
                     public Field run() throws NoSuchFieldException {
                         return tclass.getDeclaredField(fieldName);
                     }
                 });
             modifiers = field.getModifiers();
             sun.reflect.misc.ReflectUtil.ensureMemberAccess(
                 caller, tclass, null, modifiers);
             // 获取类加载器
             ClassLoader cl = tclass.getClassLoader();
             ClassLoader ccl = caller.getClassLoader();
             if ((ccl != null) && (ccl != cl) &&
                 ((cl == null) || !isAncestor(cl, ccl))) {
                 sun.reflect.misc.ReflectUtil.checkPackageAccess(tclass);
             }
         } catch (PrivilegedActionException pae) {
             throw new RuntimeException(pae.getException());
         } catch (Exception ex) {
             throw new RuntimeException(ex);
         }
         // 类型必须为int
         if (field.getType() != int.class)
             throw new IllegalArgumentException("Must be integer type");
         // 修饰符中应该包含volatile
         if (!Modifier.isVolatile(modifiers))
             throw new IllegalArgumentException("Must be volatile type");
         // 这里不是很理解,暂时理解成cclass的更新吧
         this.cclass = (Modifier.isProtected(modifiers) &&
                        tclass.isAssignableFrom(caller) &&
                        !isSamePackage(tclass, caller))
                       ? caller : tclass;
         this.tclass = tclass;
         this.offset = U.objectFieldOffset(field);  // 计算字段在对象中的offset
      }
      

compareAndSet()方法

  • 在AtomicIntegerFieldUpdater抽象类中, compareAndSet() 方法是一个抽象方法
  • AtomicIntegerFieldUpdaterImpl实现类,实现了该方法
  • 传入实例对象、预期值、新值,然后调用Unsafe类compareAndSwapInt()方法完成该字段的原子更新
    public final boolean compareAndSet(T obj, int expect, int update) {
        accessCheck(obj);
        return U.compareAndSwapInt(obj, offset, expect, update);
    }
    
  • 这里传入了一个实例对象,而不是在创建更新器时就指定实例对象
  • 这样的话,一个更新器,可以更新多个实例对象中的指定字段,而不是为每个实例对象单独创建更新器(设计巧妙而且灵活
  • 这也回答了上面的疑问:为啥不是直接传入一个引用类型(实例对象),而是传入所在类的Class对象?

实用方法

  • 对int字段的更新和直接更新int类型的变量,实际是一样的

  • AtomicIntegerFieldUpdater也提供了很多实用方法,使用时无需添加额外的循环CAS代码

  • 与其他操作int类型的原子类一样,这些实用方法实际调用Unsafe类getAndAddInt()方法

  • 返回旧值

    public final int getAndAdd(T obj, int delta)
    public final int getAndIncrement(T obj)
    public final int getAndDecrement(T obj) 
    public final int getAndSet(T obj, int newValue) 
    
  • 返回新值

    public final int addAndGet(T obj, int delta)
    public final int incrementAndGet(T obj)
    public final int decrementAndGet(T obj)
    

如何实现字段的原子更新

  • 通过对AtomicIntegerFieldUpdater类的学习,发现原子更新字段类与其他原子操作类不一样

  • 原子更新字段的相关类都是抽象类,需要通过工具方法newUpdater() 创建对应的更新器

  • 因此,基于AtomicIntegerFieldUpdater类,原子更新int字段的diamante示例如下

    public class FieldUpdater {
        private static class Student{
            private String name;
            public volatile int age;
    
            public Student(String name, int age) {
                this.name = name;
                this.age = age;
            }
        }
    
        public static void main(String[] args) {
            AtomicIntegerFieldUpdater<Student> updater = AtomicIntegerFieldUpdater.newUpdater(Student.class, "age");
            Student student = new Student("lucy", 21);
            // 新的一年,年龄增加一岁
            System.out.println(student.name + "今年" + updater.incrementAndGet(student)  + "岁");
        }
    }
    

4. 总结

好多资料都说有13个原子操作类,但是自己只发现了12个,欢迎大家与我交流 ~

  • 原子操作:执行一个操作,需要一步或多步,要求这些步骤不可分割且必须顺序执行

  • 结合统计地铁进站人数的实例,理解原子操作的重要性

    • volatile可以禁止指令重排序、保证多线程下变量的可见性,但不能保证非原子操作的线程安全
    • i++自增操作不是原子操作
    • synchronized同步能保证线程安全,但是效率大打折扣,等同于只有一个计数器的情况
    • 通过原子类AtomicInteger实现线程安全的计数器(如何实现循环CAS)
  • CAS实现原子操作的三大问题及解决方法:

    • ABA问题,使用带码版本号的原子类AtomicStampReference解决
    • 循环时间过长的问题:没有说解决方法,只说了JVM支持操作系统pause指令的好处
    • 只能原子更新一个共享变量的问题:多个共享变量放到一个类中,通过AtomicReference类对整个引用类型的原子更新,从而实现对多个共享变量的原子更新
  • Java的Atomic包:java.util.concurrent.atomic

    • Unsafe类是基础,提供了原子更新int、long和对象的native方法
    • 原子更新基本类型:boolean、int和long
    • 原子更新数组:int、long和引用类型
    • 原子更新引用类型:引用类型、带版本号的引用类型、带标记的引用类型
    • 原子更新字段:int、long和引用类型
  • 一些注意事项:

    • 每个原子类都提供了compareAndSet() 方法,尝试进行一次CAS操作,并不一定能成功
    • 原子类中的实用方法,实际调用的是Unsafe中已经实现了循环CAS的相关方法,如getAndAddInt()
    • 实用方法的分类:返回旧值(get...)、返回新值(...AndGet
  • 除了循环CAS可以实现原子,锁机制也可以实现原子操作

    • 除了偏向锁,JVM获取锁都使用了循环CAS的方式(自旋锁) —— 具体待验证

参考链接