Atomic与CAS以及产生的ABA问题
什么是原子操作
原子(atom)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为”不可被中断的一个或一系列操作” 。Java里是如何实现原子操作
在java中可以通过锁和循环CAS的方式来实现原子操作。
JVM中的CAS操作正是利用了处理器提供的CMPXCHG指令实现的。自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止,具体的类可以参见juc下的 atomic包内的原子类。
Atomic
在Atomic包里一共有12个类,四种原子更新方式,分别是原子更新基本类型,原子更新数组,原子更新引用和原子更新字段。
Atomic包里的类基本都是使用Unsafe实现的包装类。
- 基本类:AtomicInteger、AtomicLong、AtomicBoolean;
- 引用类型:AtomicReference、AtomicReference的ABA实例、
AtomicStampedRerence、AtomicMarkableReference; - 数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
- 属性原子修改器(Updater):AtomicIntegerFieldUpdater、
AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
先来看个例子了解下
public class T1_AtomicInteger {
public static int total = 0;
static AtomicInteger atomic = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(10);
for(int i=0;i<10;i++){
new Thread(()->{
for(int j=0;j<1000;j++){
/*synchronized () {
total++;//cas
}*/
atomic.getAndIncrement();
}
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
System.out.println(atomic.get());
}
}
这里的AtomicInteger的自增方法是无锁操作,也就是CAS。效率性能上都要比synchronized
要好。
CAS
CAS就是compare and swap。比较与交换。那我们通过AtomicInteger分析
do {
oldvalue = this.getIntVolatile(var1, var2);//读AtomicInteger的value值
///valueOffset---value属性在对象内存当中的偏移量
} while(!this.compareAndSwapInt(AtomicInteger, valueOffset, oldvalue, oldvalue + 1));
return var5;
读到主内存的值M,复制到工作线程中变为N,比较主内存的M和工作线程的值N是否相等,不相等自旋直到相等。相等的话更新为计算值V。
什么叫偏移量?
要用cas修改某个对象属性的值->,首先要知道属性在对象的内存空间的哪个位置,必须知道属性的偏移量
内存中的寻址
给大家看个手写CAS例子
public class AtomicStudentAgeUpdater {
private String name ;
private volatile int age;
public AtomicStudentAgeUpdater(String name,int age){
this.name = name;
this.age = age;
}
public int getAge(){
return this.age;
}
public static void main(String[] args) {
AtomicStudentAgeUpdater updater = new AtomicStudentAgeUpdater("杨过",18);
System.out.println(ClassLayout.parseInstance(updater).toPrintable());
updater.compareAndSwapAge(18,56);
System.out.println("真实的杨过年龄---"+updater.getAge());
}
private static final Unsafe unsafe = UnsafeInstance.reflectGetUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset(AtomicStudentAgeUpdater.class.getDeclaredField("age"));
System.out.println("valueOffset:--->"+valueOffset);
} catch (Exception e) {
throw new Error(e);
}
}
public void compareAndSwapAge(int old,int target){
unsafe.compareAndSwapInt(this,valueOffset,old,target);
}
}
运行结果:
CAS的写法是通用的
- unsafe类取偏移量
- unsafe.compareAndSwapInt
Automic三大API
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);
如果说要原子修改的属性是一个Array?
AtomicIntegerArray
public class AtomicIntegerArrayRunner {
static int[] value = new int[]{1,2};
static AtomicIntegerArray aiArray = new AtomicIntegerArray(value);
public static void main(String[] args) {
//todo 原子修改数组下标0的数值
aiArray.getAndSet(0,3);
System.out.println(aiArray.get(0));
System.out.println(value[0]);
}
}
运行结果:
可以看到修改成功,但是原数组的值并没有改变。因为改的是副本:我们看源码
public AtomicIntegerArray(int[] array) {
// Visibility guaranteed by final field guarantees
this.array = array.clone();
}
AtomicReferenceArray
如果是对象数组怎么改:
@Data
public class Tuling {
private Integer sequence;
public Tuling(Integer seq){
sequence = seq;
}
}
public class AtomicReferenceArrayRunner {
static Tuling[] ovalue = new Tuling[]{new Tuling(1),new Tuling(2)};
static AtomicReferenceArray<Tuling> objarray = new AtomicReferenceArray(ovalue);
public static void main(String[] args) {
System.out.println(objarray.get(0).getSequence());
objarray.set(0,new Tuling(3));
System.out.println(objarray.get(0).getSequence());
}
}
运行结果:
AtomicIntegerFieldUpdater
public class AtomicIntegerFieldUpdateRunner {
static AtomicIntegerFieldUpdater aifu = AtomicIntegerFieldUpdater.newUpdater(Student.class,"old");
public static void main(String[] args) {
Student stu = new Student("杨过",18);
//getAndIncrement返回的是原值,但是计算后的结果需要通过get方式获取
System.out.println(aifu.getAndIncrement(stu));
System.out.println(aifu.getAndIncrement(stu));
System.out.println(aifu.get(stu));
}
static class Student{
private String name;
public volatile int old;
public Student(String name ,int old){
this.name = name;
this.old = old;
}
public String getName() {
return name;
}
public int getOld() {
return old;
}
}
}
运行结果:
AtomicReferenceFieldUpdater
public class AtomicReferenceFieldUpdaterRunner {
static AtomicReferenceFieldUpdater atomic = AtomicReferenceFieldUpdater.newUpdater(Document.class,String.class,"name");
public static void main(String[] args) {
Document document = new Document("杨过",1);
System.out.println(atomic.get(document));
atomic.getAndSet(document,"xiaolongnv");
System.out.println(atomic.get(document));
}
@Data
static class Document{
public volatile String name;
private int version;
Document(String obj,int v){
name = obj;
version = v;
}
}
}
运行结果:
ABA问题及解决
举个场景:
- 王百万: 打算往自己账户100w,先查一下自w己账户:有多少钱->100W,在柜台查的,撩妹->耽误时间(1小时),
- 撩妹聊完了,又查了一下自己的户头,100w;
- 张三:去老王账户100w->非法转入股票市场户口(此时老王账户0)->炒股做T的高手(低买高卖)–>150w->100W又转回老王的户头,张三赚了50w
ABA-》怎么解决?
可以通过版本号解决:每次修改版本号加1
A-0->B-1->A-2->B-3->A-4
AtomicStampedReference
登场了
public class AtomicStampedRerenceRunner {
private static AtomicStampedReference<Integer> atomicStampedRef =
new AtomicStampedReference<>(1, 0);//初始值,版本号
public static void main(String[] args){
Thread main = new Thread(() -> {
int stamp = atomicStampedRef.getStamp(); //获取当前标识别
System.out.println("操作线程" + Thread.currentThread()+ "stamp="+stamp + ",初始值 a = " + atomicStampedRef.getReference());
try {
Thread.sleep(3000); //等待1秒 ,以便让干扰线程执行
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean isCASSuccess = atomicStampedRef.compareAndSet(1,2,stamp,stamp +1); //此时expectedReference未发生改变,但是stamp已经被修改了,所以CAS失败
System.out.println("操作线程" + Thread.currentThread() + "stamp="+stamp + ",CAS操作结果: " + isCASSuccess);
},"主操作线程");
Thread other = new Thread(() -> {
int stamp = atomicStampedRef.getStamp();
atomicStampedRef.compareAndSet(1,2,stamp,stamp+1);
System.out.println("操作线程" + Thread.currentThread() + "stamp="+atomicStampedRef.getStamp() +",【increment】 ,值 a= "+ atomicStampedRef.getReference());
stamp = atomicStampedRef.getStamp();
atomicStampedRef.compareAndSet(2,1,stamp,stamp+1);
System.out.println("操作线程" + Thread.currentThread() + "stamp="+atomicStampedRef.getStamp() +",【decrement】 ,值 a= "+ atomicStampedRef.getReference());
},"干扰线程");
main.start();
LockSupport.parkNanos(1000000);
other.start();
}
}
线程main先执行,线程other后执行。干扰线程执行完,主线程最后修改会发现修改失败。运行结果:
内存屏障
在Java 8中引入,用于定义内存屏障(也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的 所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。volatile
就是用到了内存屏障,避免了指令重排