无锁机制下的原子性操作
通常使用volatile关键字修饰字段可以实现多个线程的可见性和读写的原子性,但是对于字段的复杂性操作就需要使用synchronize关键字来进行,例如:
public class Counter { private volatile int count = 0; public synchronized int getAndIncr() { return this.count ++; } }
这里可以看到,对于字段的简单设置和获取,volatile可以应付,但是我们想每次获取后自增加1,这样的操作就只能交给synchronize来做,这样做虽然可以但是性能较低,所以我们这里介绍一种新的无锁操作CAS。
什么是CAS(Compare And Swap)
比较并且交换内存地址的数据,由CPU在指令级别上进行操作的原子性。
CAS包含三个参数:1、变量所在的地址A 2、变量应该的值V1 3、我们需要改的值V2。
如果说变量地址A上的值为V1,就用V2进行复制;如果值不是V1,则什么都不操作,返回V1的值。
自旋操作:在一个死循环里不停的进行CAS的操作,直到成功为止。
public class AtomicityTest implements Runnable { private int i = 0; public int getValue() { return i; } private synchronized void evenIncrement() { i++; i++; } public void run() { while(true) evenIncrement(); } public static void main(String[] args) { ExecutorService exec = Executors.newCachedThreadPool(); AtomicityTest at = new AtomicityTest(); exec.execute(at); while(true) { int val = at.getValue(); if(val % 2 != 0) { System.out.println(val); System.exit(0); } } } }
输出:
1
上面这段代码中,主线程循环获取线程AtomicityTest中递增的值,但是递增每次增加2(在java里i++这样的操作也不是原子性操作,为了方便演示效果自增2),主线程循环获取并判断,如果是奇数则退出循环。
最终结果是输出了奇数1,说明在evenIncrement方法中当自增1次之后被主线程获取了值,才会导致这样的情况发生。这里只有evenIncrement方法是synchronized,而getValue方法不是所以导致主程序退出。按照原先的方法只需在getValue方法上也加上synchronized即可解决。但现在有了原子操作类,可以通过以下方法得以解决:
public class AtomicIntegerTest implements Runnable { private AtomicInteger i = new AtomicInteger(0); public int getValue() { return i.get(); } private void evenIncrement() { i.addAndGet(2); } public void run() { while(true) evenIncrement(); } public static void main(String[] args) { new Timer().schedule(new TimerTask() { public void run() { System.err.println("Aborting"); System.exit(0); } }, 5000); ExecutorService exec = Executors.newCachedThreadPool(); AtomicIntegerTest ait = new AtomicIntegerTest(); exec.execute(ait); while(true) { int val = ait.getValue(); if(val % 2 != 0) { System.out.println(val); System.exit(0); } } } }
这里使用原子操作类AtomicInteger,然后在evenIncrement方法里使用addAndGet方法实现自增2,可以看出该方法也没有使用synchronized关键字,因为方法里只有原子自增,所以不需要synchronized关键字。
由于主线程是死循环,我这里使用Timer类在5秒后退出。
CAS实现原子操作的三大问题
- 当线程想把地址A的值V1改为V3时,当线程取到V1的值后再进行比较时,地址A的值从V1->V2->V1进行了改变,虽然V3可以正常赋值,但是比较的V1值已经不是取出来的V1了。
- 循环时间很长的话,CPU的负荷较大。
- 对一个变量进行操作可以,同时操作多个共享的变量有些麻烦。
CAS线程安全
通过硬件层面的阻塞实现原子操作的安全性。
常用的原子操作类
- 更新基本类型类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
- 更新数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
- 更新引用类:AtomicReference,AtomicReferenceFieldUpdater,AtomicMarkableReference
- 原子更新字段类:AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,AtomicStampedReference
public class AtomicArray { static int[] value = new int[]{1, 2}; static AtomicIntegerArray aia = new AtomicIntegerArray(value); public static void main(String[] args) { aia.set(0, 3); System.out.println(aia.get(0)); System.out.println(value[0]); } }
输出:
3 1
可以看出AtomicIntegerArray对象将int数组复制了一份,他的改变并没有影响到原有数组。
public class AtomicRef { static class User { private String name; private int age; public User(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } } static AtomicReference<User> userAtomicRef = new AtomicReference<AtomicRef.User>(); public static void main(String[] args) { User user = new User("Mark", 25); userAtomicRef.set(user); User updUser = new User("Mark", 28); userAtomicRef.compareAndSet(user, updUser); System.out.println(userAtomicRef.get().getName()); System.out.println(userAtomicRef.get().getAge()); } }
输出:
Mark 28
这里会首先进行比较,然后更新不一样的字段。
这里对CAS三大问题的第一个问题进行解决:
AtomicMarkableReference这个类会在数据被修改之后,标记位会由true变为false,判断这个标记位就可得知。
AtomicStampedReference这个类会在数据上添加时间戳,如果有数据修改时间戳会变掉,从而判断数据是否被修改。
本文索引关键字:
CAS:http://www.cnblogs.com/huanStephen/p/8213678.html#CAS
欢迎大家索引!