并发编程之原子操作Atomic&Unsafe

原子操作:507383170不能被分割(中断)的一个或一系列操作叫原子操作。

原子操作Atomic主要有12个类,4种类型的原子更新方式,原子更新基本类型,原子更新数组,原子更新字段,原子更新引用。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,注意:这里返回的是自前的值。void lazySet(int newValue):最终会设置成newValue,使用lazySet设置后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。 int getAndSet(int newValue):以原子方式设置为newValue的值,并返回值。 Atomic包提供了三种基本类型的原子更新,但是Java的基本类型里还有char,fldouble等。那么问题来了,如何原子的更新其他的基本类型呢?Atomic包里的类基本使用Unsafe实现的,Unsafe只提供了三种CAS方法,compareAndSwapObject, compareAndSwapInt和compareAndSwapLong,再看AtomicBoolean源码,发现先把Boolean转换成整型,再使用compareAndSwapInt进行CAS,所以原子更新dou也可以用类似的思路来实现。

下面我们来看一下每种类型的一个实例:

复制代码
/**  
* 

Title: AtomicIntegerTest.java

*

Description:

*

Copyright: NTT DATA Synergy All Rights Reserved.

*

Company: www.synesoft.com.cn

*

@datetime 2019年8月9日 上午8:01:30

*

$Revision$

*

$Date$

*

$Id$

*/ package com.test; import java.util.concurrent.atomic.AtomicInteger; /** * @author hong_liping * */ public class AtomicIntegerTest { static AtomicInteger ai=new AtomicInteger(); public static void main(String[] args) { for(int i=0;i<10;i++){ new Thread(new Runnable() { @Override public void run() { ai.incrementAndGet(); } }).start(); } // try { // Thread.sleep(100); // } catch (InterruptedException e) { // e.printStackTrace(); // } System.out.println("循环后的结果如下:"+ai.get()); } }
//测试结果
循环后的结果如下:9
循环后的结果如下:10
复制代码

根据上面的代码,我们多运行几次,会发现,代码的测试结果一会儿是9一会儿是10,不是10,为什么呢,因为线程还没有跑完,我下面的就已经打出来了,让线程睡眠一下就可以解决这个问题了。

下面我们来看一下atomic的ABA问题,这个问题在面试的时候经常问到。

复制代码
/**  
* 

Title: AtomicTest.java

*

Description:

*

@datetime 2019年8月8日 下午3:40:37

*

$Revision$

*

$Date$

*

$Id$

*/ package com.test; import java.util.concurrent.atomic.AtomicInteger; /** * @author hong_liping * */ public class AtomicAbaTest { private static AtomicInteger ato=new AtomicInteger(1); public static void main(String[] args) { Thread mainT=new Thread(new Runnable() { @Override public void run() { int a=ato.get(); System.out.println(Thread.currentThread().getName()+"原子操作修改前数据"+a); try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } boolean successFlag=ato.compareAndSet(a, 2); if(successFlag){ System.out.println(Thread.currentThread().getName()+"原子操作修改后数据"+ato.get()); } } },"mainT"); Thread otherT=new Thread(new Runnable() { @Override public void run() { int b=ato.incrementAndGet();//1+1 System.out.println(Thread.currentThread().getName()+"原子操作自增后数据"+b); b=ato.decrementAndGet();//2-1 System.out.println(Thread.currentThread().getName()+"原子操作自减后数据"+b); } },"OtherT"); mainT.start(); otherT.start(); } }

测试结果:

OtherT原子操作自增后数据2
mainT原子操作修改前数据1
OtherT原子操作自减后数据1
mainT原子操作修改后数据2

 
复制代码

根据上面的操作,我们可以看到的是AtomicInteger的操作自增,自减,值的替换等。但是此处应当注意的是原子操作存在一个ABA问题,ABA问题的现象就是:mainT执行完成后的值2(替换的2),otherT在执行2-1的时候的2是自增(1+1)的结果。在这两个线程中用到的2不是同一个2,就相当于是一个漏洞,相当于说你从王健林账号中偷走了10个亿去投资,等你投资好了回本了,你再把这10个亿打回了王健林账号,这整个过程王建林没有发现,你的整个操作过程也没有记录,所以对于王健林来说他的钱没有丢失过,还是放在那里的。很明显要解决这个ABA问题最好的办法就是每一步操作都打个标记,相当于一个银行的流水,这样你偷钱,还钱的整个过程就有一个出,一个入,王健林看的时候就会发现我的总金没有变,但是操作记录显示我的钱曾经被人盗了然后又被人还回来了。这就需要用到AtomicStampeReference.

2、原子更新引用类型

原子更新基本类型的AtomicInteger,只能更新一个变量,如果要原子的更新多个变 量,就需要使用这个原子更新引用类型提供的类。Atomic包提供了以下三个类: AtomicReference:原子更新引用类型。 AtomicReferenceFieldUpdater:原子更新引用类型里的字段。 AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子的更 新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef, boolean initialMark)

接下来我们来看一下AtomicStampedReference的测试类:

复制代码
/**  
* 

Title: AtomicStampedReference.java

*

Description:

*

@datetime 2019年8月9日 上午8:35:56

*

$Revision$

*

$Date$

*

$Id$

*/ package com.test; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicStampedReference; /** * @author hong_liping * */ public class AtomicStampedReferenceTest { private static AtomicStampedReference asf=new AtomicStampedReference(1, 0); public static void main(String[] args) { Thread mainT=new Thread(new Runnable() { @Override public void run() { int stamp= asf.getStamp(); System.out.println(Thread.currentThread().getName()+"原子操作修改前数据"+asf.getReference()+ "_"+stamp); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //此时expectedReference未发生改变,但是stamp已经被修改了,所以CAS失败 boolean successFlag=asf.compareAndSet(1, 2, stamp, stamp+1); if(successFlag){ System.out.println(Thread.currentThread().getName()+"原子操作修改后数据"+asf.getReference()+ "_"+stamp); }else{ System.out.println(Thread.currentThread().getName()+"cas操作失败"); } } },"mainT"); Thread otherT=new Thread(new Runnable() { @Override public void run() { int stamp=asf.getStamp(); asf.compareAndSet(1, 2, stamp, stamp+1); System.out.println(Thread.currentThread().getName()+"原子操作自增后数据"+asf.getReference()+ "_"+asf.getReference()); asf.compareAndSet(2, 1, stamp, stamp+1); System.out.println(Thread.currentThread().getName()+"原子操作自减后数据"+asf.getReference()+ "_"+stamp);; } },"OtherT"); mainT.start(); otherT.start(); } } //测试结果: mainT原子操作修改前数据2_0 OtherT原子操作自增后数据2_2 OtherT原子操作自减后数据2_0 mainTcas操作失败
复制代码
3、原子更新数组类
  通过原子的方式更新数组里的某个元素,Atomic包提供了以下三个类AtomicIntegerArray:原子更新整型数组里的元素。AtomicLongArray:原子更新长整型数组里的元素。 AtomicReferenceArray:原子更新引用类型数组里的元素。 omicIntegerArray类主要是提供原子的方式更新数组里的整型,其常用方法int addAndGet(int i, int delta):以原子方式将输入值与数组中索加。boolean compareAndSet(int i, int expect, int update):如果值,则以原子方式将数组位置i的元素设置成update值。

接下来我们来看一下AtomicIntegerArray的一个案例

复制代码
/**  
* 

Title: AtomicArrayTest.java

*

Description:

*

@datetime 2019年8月10日 上午9:45:49

*

$Revision$

*

$Date$

*

$Id$

*/ package com.test; import java.util.concurrent.atomic.AtomicIntegerArray; import com.sun.org.apache.bcel.internal.generic.NEWARRAY; /** * @author hong_liping * */ public class AtomicArrayTest { static int[] array=new int[]{1,2,3}; static AtomicIntegerArray aia=new AtomicIntegerArray(array); public static void main(String[] args) { aia.getAndSet(1, 5); System.out.println(aia.get(1)); System.out.println(array[1]); if(aia.get(1)==array[1]){ System.out.println("数组中的值与原子数组中的相等"); }else{ System.out.println("数组中的值与原子数组中的不相等"); } } }
结果:

5
2
数组中的值与原子数组中的不相等

 
复制代码

由以上的代码可以看出原子数组与我本身定义的数据同一个下标下的值是不一样的,为什么呢,我们看一下源码就会发现原子数据操作的并不是我定义的变量本身,而是先拷贝一份,然后操作的是拷贝的版本。

复制代码
 public AtomicIntegerArray(int[] array) {
        // Visibility guaranteed by final field guarantees
        this.array = array.clone();//初始化数组的时候拷贝
    }
public final int getAndSet(int i, int newValue) {
        return unsafe.getAndSetInt(array, checkedByteOffset(i), newValue);
    }
 
  
复制代码

在进行数据原子操作的时候使用的是魔术类Unsafe.

4、原子更新字段类

如果我们只需要某个类里的某个字段,那么就需要使用原子更新字段类,Atomic包提
供了以下三个类:
AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
AtomicLongFieldUpdater:原子更新长整型字段的更新器。
AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值
与引用关联起来,可用于原子的更数据和数据的版本号,可以解决使用CAS进行原子
更新时,可能出现的ABA问题。原子更新字段类都是抽象类,每次使用都时候必须使用静态方法newUpdater创建一个
更新器。原子更新类的字段的必须使用public volatile修饰符。

接下来我们再来看看AtomicIngerFieldUpdater

复制代码
/**  
* 

Title: AtomicIntegerFieldUpdateTest.java

*

Description:

*

@datetime 2019年8月10日 上午10:02:22

*

$Revision$

*

$Date$

*

$Id$

*/ package com.test; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; /** * @author hong_liping * */ public class AtomicIntegerFieldUpdateTest { static AtomicIntegerFieldUpdater aifu=AtomicIntegerFieldUpdater.newUpdater(Person.class, "age"); static class Person{ private String name; public volatile int age; public Person(String name,int age){ this.name=name; this.age=age; } public int getAge(){ return age;
并发编程之原子操作Atomic&Unsafe_第1张图片 并发编程之原子操作Atomic&Unsafe_第2张图片 并发编程之原子操作Atomic&Unsafe_第3张图片 并发编程之原子操作Atomic&Unsafe_第4张图片 并发编程之原子操作Atomic&Unsafe_第5张图片 并发编程之原子操作Atomic&Unsafe_第6张图片
 
 

你可能感兴趣的:(并发编程之原子操作Atomic&Unsafe)