Loading

Java 原子操作类

CAS(Compare And Swap)比较并更新指令级别保证这是一个原子操作,三个运算符: 一个内存地址V,一个期望值A,一个新值B;基本思路:如果地址V上的值和期望值A相等,就把新值B更新到内存,如果不是,循环(死循环,自旋)里不断的进行CAS操作;

如下图:

 

 

如果需要获取原子操作类的值并更新,期望值与内存地址中的值不等,则循环(死循环,自旋)里不断的进行CAS操作;(Atomic类的实现)

AtomicIntegergetAndIncrement()方法

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

  

UnsafegetAndAddInt方法,这种就是CAS的实现方式;

 public final int getAndAddInt(Object var1, long var2, int var4) {
     int var5;
     //如果var1和var5值不等,会重新进入循环
     do {
         var5 = this.getIntVolatile(var1, var2);
     } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
 ​
     return var5;
 }

var1:AtomicInteger对象本身
var2:该对象值得引用地址
var4:需要变动的值
var5:用var1和var2找到的内存中的真实值,是从主内存中拷贝到工作内存中的值
用该对象当前的值var1与var5比较,如果相等则更新新值(var5 + var4)到内存,不相等就会重新进入循环;

 

一般使用CAS是配合自旋一起使用的,通俗点说就是死循环里判断需要读写的内存位置的值(V)和预期值(A)是否相等,不相等就会重新进入循环,如果是高并发的场景,这会出现有很多请求多次循环也成功不了的情况,这给CPU带来非常大的消耗;

 

在Atomic包里一共提供了13个类,属于4种类型的更新方式,分别是原子更新基本类型,原子更新数组,原子更新引用和原子更新属性(字段);Atomic包里基本都是使用Unsafe实现的包装类;

  • 原子更新基本类型类

    AtomicBoolean:原子更新布尔类型

    AtomicInteger:原子更新整型

    AtomicLong:原子更新长整型

     

    AtomicInteger常用方法:

     int addAndGet(int delta): 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果
     
     boolean compareAndSet(int expect, int update): 如果输入的数值等于预期值,则以原子方式将该值设置为输入的值
         
     int getAndIncrement(): 以原子方式将当前值加1,注意,这里返回的是自增前的值
         
     void lazySet(int newValue): 最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还可以读取到旧的值
         
     int getAndSet(int newValue): 以原子方式设置为newValue的值,并返回旧值

 

Atomic 包提供了三种基本类型的原子更新;Atomic包里的类基本都是使用Unsafe实现的

 /**Unsafe.java
  * 每次在执行CAS操作时,线程会根据valueOffset去内存中获取当前值去跟expect的值做对比如果一致则修改并返回true,如果不一致说明有别的线程也在修改此对象的值,则返回false
  * value 表示 需要操作的对象
  * valueOffset 表示 对象(value)的地址的偏移量(通过Unsafe.objectFieldOffset(Field valueField)获取)
  * expect 表示更新时value的期待值
  * update 表示将要更新的值
  * @return 如果更新成功返回true,否则为false
  */
 public final native boolean compareAndSwapObject(Object value, long valueOffset, Object expect, Object update);
 ​
 public final native boolean compareAndSwapInt(Object value, long valueOffset, int expect, int update);
 ​
 public final native boolean compareAndSwapLong(Object value, long valueOffset, long expect, long update);

  

Unsafe参考:[https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html]

 

ABA问题

《Java并发编程实战》的15.4.4节如下:

ABA问题是一种异常现象:如果在算法中的节点可以被循环使用,那么在使用“比较并交换”指令时就可能出现这个问题(如果在没有垃圾回收机制的环境 中)。在CAS操作中将判断“V的值是否仍然为A?”,并且如果是的话就继续执行更新操作。在大多数情况下,这种判断是足够的。然而,有时候还需要知道 “自从上次看到V的值为A以来,这个值是否发生了变化?”在某些算法中,如果V值首先由A变成B,再由B变成A,那么仍然被认为发生了变化,并需要重新执 行算法中的某些步骤。

 

比如说线程1从内存位置valueOffset中取出值A,线程2也从内存位置valueOffset中取出值A,并执行一些操作将值变为B,然后再将B变成A,这时候线程1进行CAS操作发现内存中仍然是A,线程1执行成功,但不能代表整个过程中,值没有被修改过;

public class UseAtomicIntegerTest {
     static AtomicInteger integer = new AtomicInteger(1);
 ​
     public static void main(String[] args) throws InterruptedException {
         int oldValue = integer.get();
         System.out.println(Thread.currentThread().getName() + " oldValue:" + oldValue);
 ​
         Thread thread1 = new Thread(new Runnable() {
             @Override
             public void run() {
                 try {
                     System.out.println(Thread.currentThread().getName() + " --- " + integer.get());
                     Thread.sleep(2L);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
         });
 ​
         Thread thread2 = new Thread(new Runnable() {
             @Override
             public void run() {
                 // 确保别的线程1先执行
                 Thread.yield();
 ​
                 // integer + 1
                 System.out.println(Thread.currentThread().getName() + " --- integer:" + integer.incrementAndGet());
 ​
                 // integer - 1
                 System.out.println(Thread.currentThread().getName() + " --- integer:" + integer.decrementAndGet());
             }
         });
 ​
         thread1.start();
         thread2.start();
 ​
         thread1.join();
         thread2.join();
         boolean result = integer.compareAndSet(oldValue, oldValue + 10);
         System.out.println(Thread.currentThread().getName() + " --- " + integer.get() + " result:" + result);
     }
 }

 

打印如下:

 main oldValue:1
 Thread-0 --- 1
 Thread-1 --- integer:2
 Thread-1 --- integer:1
 main --- 11 result:true

  

执行最后的值是相同的,但不能代表整个过程中,值没有被修改过;

 

ABA解决方案:

给变量加一个版本号即可,在比较的时候不仅要比较当前变量的值,还需要比较当前变量的版本号; 在Java5中,已经提供了AtomicStampedReference来解决问题,检查当前引用是否等于预期引用,其次检查当前标志是否等于预期标志,如果都相等就会以原子的方式将引用和标志都设置为新值;

 

  • 原子更新数组

    AtomicIntegerArray: 原子更新整型数组里地元素

    AtomicLongArray: 原子更新长整型数组里的元素

    AtomicReferenceArray: 原子更新引用类型数组里的元素

    AtomicIntergerArray: 主要是提供原子的方式更新数组里的整型

     //以原子方式将输入值与数组中索引i的元素相加
     int addAndGet(int i, int delta);
     ​
     //如果当前值等于预期值,则以原子方式将数组位置i的元素设置成update值    
     boolean compareAndSet(int i, int expect, int update);
    

      

    AtomicIntergerArray会将当前数组复制一份,所以当AtomicIntegerArray对内部的元素进行修改时,不会影响传入的数组;

 

  • 原子更新引用类型

    AtomicReference: 原子更新引用类型

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

    AtomicMarkableReference: 原子更新带有标记位的引用类型;可以原子的更新一个布尔类型的标记位和引用类型,构造方法是AtomicMarkableReference(V initialRef, boolean initialMark)

 

  • 原子更新字段类

    AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器;

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

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

 

使用AtomicStampedReference解决ABA

 public class UseAtomicIntegerTest {
     static AtomicInteger integer = new AtomicInteger(1);
 ​
     public static void main(String[] args) throws InterruptedException {
         int oldValue = integer.get();
         System.out.println(Thread.currentThread().getName() + " oldValue:" + oldValue);
 ​
         Thread thread1 = new Thread(new Runnable() {
             @Override
             public void run() {
                 try {
                     System.out.println(Thread.currentThread().getName() + " --- " + integer.get());
                     Thread.sleep(2L);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
         });
 ​
         Thread thread2 = new Thread(new Runnable() {
             @Override
             public void run() {
                 // 确保别的线程1先执行
                 Thread.yield();
 ​
                 // integer + 1
                 System.out.println(Thread.currentThread().getName() + " --- integer:" + integer.incrementAndGet());
 ​
                 // integer - 1
                 System.out.println(Thread.currentThread().getName() + " --- integer:" + integer.decrementAndGet());
             }
         });
 ​
         thread1.start();
         thread2.start();
 ​
         thread1.join();
         thread2.join();
         System.out.println(Thread.currentThread().getName() + " --- " + integer.get());
     }
 }

  

打印结果如下:

 main oldValue:1 marked:false
 Thread-0 --- value:1 marked:false
 Thread-1 --- value:2 result:true marked:true
 Thread-1 --- value:2 marked:true
 Thread-1 --- value:1 result:true marked:true
 main --- 1 result:false

 

 CAS存在以下的缺陷:

1.ABA问题。CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”;

  • JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值;

2.循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销;

3.只能保证一个共享变量的原子操作。对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的;

  • Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作;或者使用LongAdder替代;

参考:[https://tech.meituan.com/2018/11/15/java-lock.html]

   [https://blog.csdn.net/wufagang/article/details/108239872] 

 

posted @ 2020-03-28 15:06  街头卖艺的肖邦  阅读(190)  评论(0编辑  收藏  举报