Atomic类
什么是原子操作?
原子即“不能被进一步分割的最小粒子”,原子操作(atomic operation)即”不可被中断的一个或一系列操作”,这组操作要么都执行要么都不执行,不会存在只执行一部分的情况。
处理器如何实现原子操作?
1.使用总线锁保证原子性
第一个机制是通过总线锁保证原子性。如果多个处理器同时对共享变量进行读改写(i++就是经典的读改写操作)操作,那么共享变量就会被多个处理器同时进行操作,这样读改写操作就不是原子的,操作完之后共享变量的值会和期望的不一致,举个例子:如果i=1,我们进行两次i++操作,我们期望的结果是3,但是有可能结果是2。如下图所示:
原因是有可能多个处理器同时从各自的缓存中读取变量i,分别进行加一操作,然后分别写入系统内存当中。那么想要保证读改写共享变量的操作是原子的,就必须保证CPU1读
改写共享变量的时候,CPU2不能操作缓存了该共享变量内存地址的缓存。处理器使用总线锁就是来解决这个问题的。所谓总线锁就是使用处理器提供的一个
LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占使用共享内存。
2.使用缓存一致性协议(参考前面的文章)
Java如何实现原子操作?
在java中可以通过锁和循环CAS的方式来实现原子操作。主要是Atomic包里面的类都是使用CAS操作共享变量来实现原子操作。
在Atomic包里一共有12个类,四种原子更新方式,分别是原子更新基本类型,原子更新数组,原子更新引用和原子更新字段。Atomic包里的类基本都是使用Unsafe实现的包装类。
基本类:AtomicInteger、AtomicLong、AtomicBoolean;
引用类型:AtomicReference、AtomicReference的ABA实例、AtomicStampedRerence、AtomicMarkableReference;
数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
属性原子修改器(Updater):AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
1.原子更新基本类型
用于通过原子的方式更新基本类型,Atomic包提供了以下三个类:
AtomicBoolean:原子更新布尔类型。
AtomicInteger:原子更新整型。
AtomicLong:原子更新长整型。
AtomicInteger的常用方法如下:
int addAndGet(int delta) :以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
boolean compareAndSet(int expect, int update) :如果输入的数值等于预期值,则以原子方式将该值设置为输入的值。
int getAndIncrement():以原子方式将当前值加1,注意:这里返回的是自增前的值。
int getAndSet(int newValue):以原子方式设置为newValue的值,并返回旧值。
Atomic包提供了三种基本类型的原子更新,但是Java的基本类型里还有char,float和double等。那么问题来了,如何原子的更新其他的基本类型呢?Atomic包里的类基本都是
使用Unsafe实现的,Unsafe只提供了三种CAS方法,compareAndSwapObject,compareAndSwapInt和compareAndSwapLong,再看AtomicBoolean源码,发现其是
先把Boolean转换成整型,再使用compareAndSwapInt进行CAS,所以原子更新double也可以用类似的思路来实现。
AtomicInteger的代码示例如下所示:实现的是从0增加到10
public class AtomicIntegerTest {
static AtomicInteger atomicInteger = new AtomicInteger();
public static void main(String[] args) {
/**
CyclicBarrier cyclicBarrier=new CyclicBarrier(10, new Runnable() {
@Override
public void run() {
System.out.println("自加10次数值:--->"+atomicInteger.get());
}
});
for(int i=0;i<10;i++){
new Thread(new Runnable() {
@Override
public void run() {
atomicInteger.incrementAndGet();
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}).start();
}**/
for (int i = 0; i<10; i++){
new Thread(new Runnable() {
@Override
public void run() {
atomicInteger.incrementAndGet();
}
}).start();
}
try {
Thread.sleep(1000);//防止主线程插队,可以使CyclicBarrier来实现如上所示。
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("自加10次数值:--->"+atomicInteger.get());
}
}
但是使用CAS会带来ABA问题,ABA问题的代码如下所示:
public class AtomicAbaProblemTest {
static AtomicInteger atomicInteger = new AtomicInteger(1);
public static void main(String[] args) {
Thread main = new Thread(new Runnable() {
@Override
public void run() {
int a = atomicInteger.get();
System.out.println("操作线程"+Thread.currentThread().getName()+"--修改前操作数值:"+a);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean isCasSuccess = atomicInteger.compareAndSet(a,2);
if(isCasSuccess){
System.out.println("操作线程"+Thread.currentThread().getName()+"--Cas修改后操作数值:"+atomicInteger.get());
}else{
System.out.println("CAS修改失败");
}
}
},"主线程");
Thread other = new Thread(new Runnable() {
@Override
public void run() {
atomicInteger.incrementAndGet();// 1+1 = 2;
System.out.println("操作线程"+Thread.currentThread().getName()+"--increase后值:"+atomicInteger.get());
atomicInteger.decrementAndGet();// atomic-1 = 2-1;
System.out.println("操作线程"+Thread.currentThread().getName()+"--decrease后值:"+atomicInteger.get());
}
},"干扰线程");
main.start();
other.start();
}
}
结果如下图所示:
可以看出即是干扰线程使用了atomicInterger的值,对于主线程来说他并不知道,所以当干扰线程把变量首先加1变成2,然后又把变量减1变成1,此时主线程去读取变量的值发现变量的值还是1,所以他便去更新这个值,实际上这个值已经被干扰线程修改过了,但是主线程并不知道,所以对于干扰线程来说他实现了一个1-2-1的过程,这就是典型性的ABA问题。
解决ABA问题可以使用版本号机制:代码如下所示,每次修改变量之前不仅要比较要修改的值,还要比较自己的版本号是否和原来的相同。代码如下所示:
public class AtomicStampedRerenceTest {
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(1000); //等待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】 ,值 = "+ atomicStampedRef.getReference());
stamp = atomicStampedRef.getStamp();
atomicStampedRef.compareAndSet(2,1,stamp,stamp+1);
System.out.println("操作线程" + Thread.currentThread() + "stamp="+atomicStampedRef.getStamp() +",【decrement】 ,值 = "+ atomicStampedRef.getReference());
},"干扰线程");
main.start();
other.start();
}
}
结果如下所示:
可以看到CAS更新失败,因为版本号变成了2,如果没有ABA操作,版本号应该是0,但是现在确实2
2.原子更新数组
通过原子的方式更新数组里的某个元素,Atomic包提供了以下三个类:
AtomicIntegerArray:原子更新整型数组里的元素。
AtomicLongArray:原子更新长整型数组里的元素
AtomicReferenceArray:原子更新引用类型数组里的元素
AtomicIntegerArray类主要是提供原子的方式更新数组里的整型,其常用方法如下
int addAndGet(int i, int delta):以原子方式将输入值与数组中索引i的元素相加。
boolean compareAndSet(int i, int expect, int update):如果当前值等于预期值,则以原子方式将数组位置i的元素设置成update值。
演示代码如下所示:
public class AtomicIntegerArrayTest {
static int[] value = new int[]{1,2};
static AtomicIntegerArray aiArray = new AtomicIntegerArray(value);
public static void main(String[] args) {
aiArray.getAndSet(0,3);
System.out.println(aiArray.get(0));
System.out.println(value[0]);
if(aiArray.get(0) != value[0]){
System.out.println("是否相等");
}
}
}
需要注意的是修改的是克隆的数组,并没有修改原始数组,所以原始数组的值还是没有改变。这一点可以通过源码看出:如下所示:
public AtomicIntegerArray(int[] array) {
// Visibility guaranteed by final field guarantees
this.array = array.clone();
}