原子操作是什么
原子操作即不可被中断(分割)的一个或一系列操作,如对于A,B两个操作,如果一个线程操作A,若另一个线程执行B操作时,要么将B执行到结束,要么完全不让B执行,此时对于A和B来说,就是原子的。
悲观锁和乐观锁
如synchronized
就是悲观锁,解决多线程并发问题,以保证事务的完整性。其是基于阻塞的锁机制,如果有一个线程拥有锁后,访问该资源的其他线程就需要阻塞等待,直到获得锁的线程释放锁。
CAS操作即是乐观锁,每个线程都可以访问,只有在提交数据的时候检查是否违反了数据的完整性,即每次都尝试去完成某个操作,如果冲突和造成操作失败,就循环重试,直到操作成功为止。
使用synchronized
会引出很多问题,如:获得锁的线程一直不释放锁;大量线程竞争资源,可能会造成死锁;被阻塞的线程可能优先级很高却一直无法获取锁。
因此实现原子操作可使用CAS指令。
CAS
CAS 即Compare and Swap,比较并交换。每个CAS操作过程都包括三个运算符:
内存地址V、期望值A、新的值B
如果操作的时候地址V上的值等于期望的值A,则将地址V上的值更新为B,否则不做任何操作,但要返回原值。循环CAS就是不断的进行CAS操作直到操作成功。可防止内存中共享变量出现脏读脏写的问题。
CAS是通过硬件CPU和内存,利用CPU的多处理能力实现硬件层面的阻塞,再加上volatile变量的特性来实现基于原子操作的线程安全。
(1)get变量值(旧值)----->
(2)计算后得到新值------>
(3)比较内存中变量值和旧值----->
(4)如果(3)相等,则让新值替换旧值,否则继续(1)
CAS实现原子操作的三大问题
ABA问题
由于CAS在操作值的时候需要检查旧值是否发生变化,如果未变化则更新值,但如果一个值原来是A,变成了B,然后又变为了A,这样虽然值已经发生了变化,但使用CAS进行检查时会发现它的旧值“未变化”。
而要解决该问题,就是使用版本号。即给变量添加版本号,每次更新变量都去更新它的版本号,如刚刚A->B->A,便变成了 1A->2B->3A。就像在家里倒了一杯水,你还没来得及喝,但是被家人喝掉, 然后又给你接满了一杯水,这样如果在不知道的情况下,你会认为这仍然是之前那杯水。又或者你的资金被别人挪用了,然后又还给了你,虽然钱没有变,但造成的问题时那个人已经犯罪了。
循环时间长,开销大
如果CAS一直处于自旋不成功状态,CPU的开销会大大增加。
只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,可使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,当然可以用锁。
也可以把多个共享变量合并成一个共享变量来操作。如,有两个共享变量 i=a,j=b,合并一下 ij=ab,然后用 CAS 来操作 ij,然后通过AtomicReference
类来保证引用对象之间的原子性,便实现了多个变量放在一个对象里来进行 CAS 操作。
JDK中相关原子操作类的使用
概览
更新基本类型类:AtomicBoolean
,AtomicInteger
,AtomicLong
更新数组类:AtomicIntegerArray
,AtomicLongArray
,AtomicReferenceArray
更新引用类型:AtomicReference
,AtomicMarkableReference
,AtomicStampedReference
原子更新字段类: AtomicReferenceFieldUpdater
,AtomicIntegerFieldUpdater
,AtomicLongFieldUpdater
基本数据类型的原子操作类以AtomicInteger
为例,其余差不多。
AtomicInteger
常用方法如下:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
//获取指针类Unsafe
private static final Unsafe unsafe = Unsafe.getUnsafe();
//内存偏移量
private static final long valueOffset;
static {
try {
//通过objectFieldOffset()方法,获取对象属性的偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//当前的变量值
private volatile int value;
public AtomicInteger(int initialValue) {
//初始化变量值
value = initialValue;
}
//value初始化为0
public AtomicInteger() {
}
//返回当前值
public final int get() {
return value;
}
//设置当前值
public final void set(int newValue) {
value = newValue;
}
//懒设置,使用Unsafe.putOrderedObject方法实现,实现非堵塞的写入,最终会将value置为newValue。
//该方法可能出现其他线程在未来很小的一段时间内无法获取到新值,但可获取到旧值
public final void lazySet(int newValue) {
unsafe.putOrderedInt(this, valueOffset, newValue);
}
//以原子方式设置为给定值并返回旧值。
public final int getAndSet(int newValue) {
return unsafe.getAndSetInt(this, valueOffset, newValue);
}
//以下是getAndSetInt源码,通过while循环重试更新值,直到成功,也是通过CAS实现(JDK8版本)
/*public final int getAndSetInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var4));
return var5;
}*/
//原子更新value,如果当前值等于expect,则设置为update
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
//以原子方式将当前值增加一
//返回旧值,底层同getAndSetInt一样使用CAS操作
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
//以原子方式将当前值减一
//返回旧值,CAS操作
public final int getAndDecrement() {
return unsafe.getAndAddInt(this, valueOffset, -1);
}
//以原子方式将给定值delta添加到当前值
//返回旧值,CAS操作
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
//以原子方式将当前值加1,返回新值
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
//当前值减1,返回新值
public final int decrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
}
//以原子方式将当前值增加delta,返回新值
public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}
//使用给定函数的结果以原子方式更新当前值,返回更新后的值。给的函数是无副作用的,因为当尝试更新由于线程之间的争用而失败时,它可能会重新应用
public final int updateAndGet(IntUnaryOperator updateFunction) {
int prev, next;
do {
prev = get();
next = updateFunction.applyAsInt(prev);
} while (!compareAndSet(prev, next));
return next;
}
//同上,使用给定函数的结果以原子方式更新当前值,返回更新前的值。应用该函数时,将当前值作为其第一个参数,并将给定的update作为第二个参数。
public final int getAndAccumulate(int x,
IntBinaryOperator accumulatorFunction) {
int prev, next;
do {
prev = get();
next = accumulatorFunction.applyAsInt(prev, x);
} while (!compareAndSet(prev, next));
return prev;
}
//同getAndAccumulate,但返回更新后的值
public final int accumulateAndGet(int x,
IntBinaryOperator accumulatorFunction) {
int prev, next;
do {
prev = get();
next = accumulatorFunction.applyAsInt(prev, x);
} while (!compareAndSet(prev, next));
return next;
}
}
使用演示:
public class UseAtomicInt {
static AtomicInteger atomicInteger = new AtomicInteger(10);
public static void main(String[] args) {
//返回旧值10
System.out.println(atomicInteger.getAndIncrement());
//返回新值12
System.out.println(atomicInteger.incrementAndGet());
atomicInteger.compareAndSet(12,1);
//加24 返回25
System.out.println(atomicInteger.addAndGet(24));
//自定义函数
IntBinarOperImpl intBinarOper=new IntBinarOperImpl();
//传入自定义函数,传入更新值
atomicInteger.accumulateAndGet(1, intBinarOper);
//返回26
System.out.println(atomicInteger.get());
}
}
//自定义函数,实现IntBinaryOperator接口
public class IntBinarOperImpl implements IntBinaryOperator {
@Override
public int applyAsInt(int left, int right) {
//简单定义原来的数加上要更新的数
return left+right;
}
}
AtomicIntegerArray
是提供原子的方式更新数组里的整型,还包括高级原子操作。常用方法如下:
public class AtomicIntegerArray implements java.io.Serializable {
private static final long serialVersionUID = 2862133569453604235L;
//同上
private static final Unsafe unsafe = Unsafe.getUnsafe();
//获取数组第一个元素的偏移地址
private static final int base = unsafe.arrayBaseOffset(int[].class);
//存储移位个数
private static final int shift;
//保存的数组
private final int[] array;
static {
//获取该类型的数组中元素的大小(字节)。
int scale = unsafe.arrayIndexScale(int[].class);
//scale如果不是2的次幂则抛出异常
if ((scale & (scale - 1)) != 0)
throw new Error("data type scale not a power of two");
//计算得到需要移位的个数,即scale的次幂数
shift = 31 - Integer.numberOfLeadingZeros(scale);
}
//返回第i个元素的地址
private static long byteOffset(int i) {
//shift为偏移位数,base为第一个元素的地址,i移shift个位后+基位置得到第i个元素位置
return ((long) i << shift) + base;
}
//先检测i是否越界,再调用byteOffset(i)返回
private long checkedByteOffset(int i) {
if (i < 0 || i >= array.length)
throw new IndexOutOfBoundsException("index " + i);
return byteOffset(i);
}
//构造初始化给定长度数组
public AtomicIntegerArray(int length) {
array = new int[length];
}
//创建一个新AtomicIntegerArray,其长度与传入数组相同,并从给定数组中复制所有元素。所以当 AtomicIntegerArray 对内部的数组元素进行修改时,不会影响传入的数组。
public AtomicIntegerArray(int[] array) {
this.array = array.clone();
}
//返回当前位置的元素
public final int get(int i) {
//调用checkedByteOffset获取当前位置的元素地址
return getRaw(checkedByteOffset(i));
}
//getIntVolatile方法获取数组中offset偏移地址对应的整型field的值,支持volatile load语义。返回该值。
private int getRaw(long offset) {
return unsafe.getIntVolatile(array, offset);
}
//更新第i个位置的值,也需先计算当前位置的元素的offset
public final void set(int i, int newValue) {
unsafe.putIntVolatile(array, checkedByteOffset(i), newValue);
}
//同AtomicInteger的lazySet,这里更新第i个位置的元素
public final void lazySet(int i, int newValue) {
unsafe.putOrderedInt(array, checkedByteOffset(i), newValue);
}
public final int getAndSet(int i, int newValue) {
return unsafe.getAndSetInt(array, checkedByteOffset(i), newValue);
}
//如果当前值等于预期值,则以原子方式将数组位置 i 的元素设置成 update 值。
public final boolean compareAndSet(int i, int expect, int update) {
return compareAndSetRaw(checkedByteOffset(i), expect, update);
}
private boolean compareAndSetRaw(long offset, int expect, int update) {
return unsafe.compareAndSwapInt(array, offset, expect, update);
}
//原子的方式让第i个元素加1
public final int getAndIncrement(int i) {
return getAndAdd(i, 1);
}
//原子的方式让第i个元素减1
public final int getAndDecrement(int i) {
return getAndAdd(i, -1);
}
//原子的方式让第i个元素加delta
public final int getAndAdd(int i, int delta) {
return unsafe.getAndAddInt(array, checkedByteOffset(i), delta);
}
//还有其他一些方法都和Integer原子类基本一样
简单使用:
public class AtomicArray {
static int[] value = new int[] { 1, 2,3 };
static AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(value);
public static void main(String[] args) {
//返回第一个位置的元素 2
System.out.println(atomicIntegerArray.get(1));
//更新第1个位置的元素
atomicIntegerArray.set(1,4);
//返回4
System.out.println(atomicIntegerArray.get(1));
//设置第0个位置为3,返回更新前的值 1
System.out.println( atomicIntegerArray.getAndSet(0, 3));
//返回更新后的3
System.out.println(atomicIntegerArray.get(0));
//返回1,原数组不会变化
System.out.println(value[0]);
atomicIntegerArray.compareAndSet(0,3,4);
System.out.println(atomicIntegerArray.get(0));
}
}
更新引用类型
原子更新基本类型的 AtomicInteger,只能更新一个变量,如果要原子更新多个变量,就需要使用这个原子更新引用类型提供的类。Atomic 包提供了以下 3类。AtomicReference
,AtomicStampedReference
,AtomicMarkableReference
AtomicReference
方法同前面几个类基本一样,但支持泛型,可传入对象,支持对对象的原子操作
AtomicStampedReference和AtomicMarkableReference
AtomicStampedReference
使用版本戳的记录每次改变后的版本号,这样的话就不会存在 ABA问题了。AtomicMarkableReference
跟 AtomicStampedReference
基本相同,AtomicStampedReference
在Pair
类 中使用 int
类型的stamp
作为计数器,每次操作都更新版本,关注的的是动过几次。AtomicMarkableReference
在 Pair
类 使用的是 boolean
类型的mark
,关注的的是有没有被动过。
AtomicStampedReference
源码如下:
-
首先定义了一个
Pair
内部类: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); } }
该类将元素的值(reference)和版本号(stamp)都维护在自己身上。
-
成员变量
//声明Pair类型变量 private volatile Pair<V> pair; //获取Unsafe private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe(); //使用unsafe获取偏移量保存到pairOffset private static final long pairOffset = objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class); static long objectFieldOffset(sun.misc.Unsafe UNSAFE, String field, Class<?> klazz) { try { return UNSAFE.objectFieldOffset(klazz.getDeclaredField(field)); } catch (NoSuchFieldException e) { // Convert Exception to corresponding Error NoSuchFieldError error = new NoSuchFieldError(field); error.initCause(e); throw error; //后面两个放在源码的最后,不太好找
-
构造方法
public AtomicStampedReference(V initialRef, int initialStamp) { pair = Pair.of(initialRef, initialStamp); }
传入初始值和初值版本戳保存到pair。
-
方法
4.1getReference()
和getStamp()
:返回元素值和返回版本戳public V getReference() { return pair.reference; } public int getStamp() { return pair.stamp; }
4.2
get()
//返回当前元素的值,并将当前元素的版本号保存到传入的数组中的第一个位置 public V get(int[] stampHolder) { Pair<V> pair = this.pair; stampHolder[0] = pair.stamp; return pair.reference; }
4.3
casPair()
//cmp:期望的pair,val:新的pair //CAS更新当前的pair private boolean casPair(Pair<V> cmp, Pair<V> val) { return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val); }
4.4
compareAndSet()
/** * 原子的修改元素的值和版本戳 * @param 期望值 * @param 新值 * @param 期望版本戳 * @param 新版本戳 */ 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))); }
该方法首先判断期望的值和版本戳是否和当前的值和版本戳相同,如果不相同则直接返回
false
,如果相同则继续比较新的值和版本戳是否和当前值相同,如果相同则直接返回true
,否则对当前的pair
进行CAS
操作。
4.5weakCompareAndSet()
// 同compareAndSet,但可能不能保证原子性 public boolean weakCompareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) { return compareAndSet(expectedReference, newReference, expectedStamp, newStamp); }
其代码同compareAndSet无差别,但在JDK9中,两个方法都添加了@HotSpotIntrinsicCandidate注解,使用该注解的方法在JVM中都有一套基于CPU指令的高效的实现,且会在运行时会替代JDK的源码实现,从而获得更高的效率。即HotSpot可能会手动实现这个方法。参考https://blog.csdn.net/lzcaqde/article/details/80868854的解读。
4.6set()
/** * 源码解释无条件更新当前元素的值和版本戳 * @param newReference 新值 * @param newStamp 新版本戳 */ public void set(V newReference, int newStamp) { Pair<V> current = pair; if (newReference != current.reference || newStamp != current.stamp) this.pair = Pair.of(newReference, newStamp); }
4.6
attemptStamp()
如果引用对象为期望值,则重新设置新的版本戳。
public boolean attemptStamp(V expectedReference, int newStamp) { Pair<V> current = pair; return expectedReference == current.reference && (newStamp == current.stamp || casPair(current, Pair.of(expectedReference, newStamp))); }
示例:
public class UseAtomicStampedReference {
/**
* 设置初始值和初始版本戳
*/
static AtomicStampedReference<String> asr
= new AtomicStampedReference("wml",0);
public static void main(String[] args) throws InterruptedException {
//拿到当前的版本号(旧)
final int oldStamp = asr.getStamp();
final String oldReference = asr.getReference();
System.out.println("旧值:"+oldReference+",旧版本戳:"+oldStamp);
Thread rightStampThread = new Thread(() -> {
System.out.println(Thread.currentThread().getName()+":当前变量值:"
+oldReference + ",当前版本戳:" + oldStamp );
//更新引用对象值和版本戳
asr.compareAndSet(oldReference, "wml222", oldStamp, oldStamp + 1);
int[] stampHolder=new int[2];
stampHolder[0]=0;
//调用get,返回当前值,并将版本保存到stampHolder[0]中
asr.get(stampHolder);
//打印当前版本
System.out.println("使用get()获取版本:"+stampHolder[0]);
});
Thread errorStampThread = new Thread(() -> {
String reference = asr.getReference();
System.out.println(Thread.currentThread().getName()+":当前变量值:"
+reference + ",当前版本戳:" + asr.getStamp());
//更新引用对象值和版本戳
asr.compareAndSet(reference, "wml333", oldStamp, oldStamp + 1);
});
rightStampThread.start();
rightStampThread.join();
errorStampThread.start();
errorStampThread.join();
System.out.println("Main线程:当前变量值:"
+asr.getReference() + ",当前版本戳:" + asr.getStamp());
asr.set("set新值",3);
System.out.println("使用set更新:当前变量值:"
+asr.getReference() + ",当前版本戳:" + asr.getStamp());
}
}
线程errorStampThread
没有使用更新后的版本戳作为期望版本,因此该线程CAS失败。
来源:CSDN
作者:一颗小陨石
链接:https://blog.csdn.net/weixin_43696529/article/details/104106247