Java 原子操作类
CAS(Compare And Swap),比较并更新,指令级别保证这是一个原子操作,三个运算符: 一个内存地址V,一个期望值A,一个新值B;基本思路:如果地址V上的值和期望值A相等,就把新值B更新到内存,如果不是,循环(死循环,自旋)里不断的进行CAS操作;
如下图:
如果需要获取原子操作类的值并更新,期望值与内存地址中的值不等,则循环(死循环,自旋)里不断的进行CAS操作;(Atomic
类的实现)
如AtomicInteger
的getAndIncrement()方法
public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); }
Unsafe
的getAndAddInt方法,这种就是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]