zoukankan      html  css  js  c++  java
  • 并发编程之原子操作Atomic&Unsafe

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

    原子操作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也可以用类似的思路来实现。

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

    /**  
    * <p>Title: AtomicIntegerTest.java</p >  
    * <p>Description: </p >  
    * <p>Copyright: NTT DATA Synergy All Rights Reserved.</p >  
    * <p>Company: www.synesoft.com.cn</p >  
    * <p>@datetime 2019年8月9日 上午8:01:30</p >
    * <p>$Revision$</p > 
    * <p>$Date$</p >
    * <p>$Id$</p >
    */  
    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问题,这个问题在面试的时候经常问到。

    /**  
    * <p>Title: AtomicTest.java</p >  
    * <p>Description: </p >  
    * <p>@datetime 2019年8月8日 下午3:40:37</p >
    * <p>$Revision$</p > 
    * <p>$Date$</p >
    * <p>$Id$</p >
    */  
    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的测试类:

    /**  
    * <p>Title: AtomicStampedReference.java</p >  
    * <p>Description: </p >  
    * <p>@datetime 2019年8月9日 上午8:35:56</p >
    * <p>$Revision$</p > 
    * <p>$Date$</p >
    * <p>$Id$</p >
    */  
    package com.test;
    
    import java.util.concurrent.atomic.AtomicInteger;
    import java.util.concurrent.atomic.AtomicStampedReference;
    
    /**
     * @author hong_liping
     *
     */
    public class AtomicStampedReferenceTest {
    
         private static AtomicStampedReference<Integer> asf=new AtomicStampedReference<Integer>(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的一个案例

    /**  
    * <p>Title: AtomicArrayTest.java</p >  
    * <p>Description: </p >  
    * <p>@datetime 2019年8月10日 上午9:45:49</p >
    * <p>$Revision$</p > 
    * <p>$Date$</p >
    * <p>$Id$</p >
    */  
    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

    /**  
    * <p>Title: AtomicIntegerFieldUpdateTest.java</p >  
    * <p>Description: </p >    
    * <p>@datetime 2019年8月10日 上午10:02:22</p >
    * <p>$Revision$</p > 
    * <p>$Date$</p >
    * <p>$Id$</p >
    */  
    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;
          }
        }
        
        
        public static void main(String[] args) {
                Person person=new Person("张三", 18);
                System.out.println(aifu.getAndIncrement(person));
                System.out.println(aifu.get(person));
        }
        
    }

    测试结果:

    18
    19

    在age属性上加volatile是为了保证在多线程并发的情况下保证可见性。

    Unsafe

    Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。 Unsafe类为一单例实现,提供静态方法getUnsafe获取Unsafe实例,当且仅当调用getUnsafe方法的类为引导类加载器所加载时才合法,否则抛出SecurityException异常。

    @CallerSensitive
    /*      */   public static Unsafe getUnsafe()
    /*      */   {
    /*   88 */     Class localClass = Reflection.getCallerClass();
    /*   89 */     if (!VM.isSystemDomainLoader(localClass.getClassLoader()))// 仅在引导类加载器`BootstrapClassLoader加载时才合法
    /*   90 */       throw new SecurityException("Unsafe");
    /*   91 */     return theUnsafe;
    /*      */   }
    /*      */   

    Unsafe经常用到的就是CAS,内存屏障(禁止load,store重新排序),线程调度(线程挂起,恢复还有获取,释放锁)。

    如何获取Unsafe,1、把调用Unsafe相关方法的类Demo所在jar包路径追加到默认的bootstrap路径中,使得A被引导类加载器加载 java -Xbootclasspath/Demo:${path} // 其中path为调用Unsafe相关方法的类所在jar包路径

    2、通过反射获取单例对象theUnsafe

    我们可以看一下下面的一个代码:

    public class UnsafeInstance {
        public static Unsafe reflectGetUnsafe(){
            Field field;
            try {
                field = Unsafe.class.getDeclaredField("theUnsafe");
                field.setAccessible(true);
                return (Unsafe) field.get(null);
            } catch (Exception e) {            
                e.printStackTrace();
            }
            return null;
        }
    }

    接下来再来看一个利用Unsafe的代码:

    /**  
    * <p>Title: AtomicUnsafeUpdaterTest.java</p >  
    * <p>Description: </p >  
    * <p>@datetime 2019年8月10日 上午10:57:23</p >
    * <p>$Revision$</p > 
    * <p>$Date$</p >
    * <p>$Id$</p >
    */  
    package com.test;
    
    import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
    
    import sun.misc.Unsafe;
    
    /**
     * @author hong_liping
     *
     */
    public class AtomicUnsafeUpdaterTest {
        private String name;
        private volatile int age;
        private static final Unsafe unsafe=UnsafeInstance.reflectGetUnsafe();
        private static final long valueOffset;
        static{
            try {
                valueOffset=unsafe.objectFieldOffset(AtomicUnsafeUpdaterTest.class.getDeclaredField("age"));//偏移量
                System.out.println("initial valueOffset is "+valueOffset);
            } catch (Exception e) {
                throw new Error(e);
            } 
        }
       public void compareAndSwapAge(int old,int target){
           unsafe.compareAndSwapInt(this, valueOffset, old, target);
       }
    
       public AtomicUnsafeUpdaterTest(String name,int age){
           this.name=name;
           this.age=age;
       }
       public int getAge(){
           return this.age;
       }
       public static void main(String[] args) {
        AtomicUnsafeUpdaterTest test=new AtomicUnsafeUpdaterTest("美女",30);
        test.compareAndSwapAge(30, 25);
        System.out.println("年龄变换后的值为"+test.getAge());
    }
    }

    1、CAS(unsafe的用法)的几个重要方法以及参数:

    /** * CAS
    * @param o 包含要修改field的对象
    * @param offset 对象中某field的偏移量
    * @param expected 期望值
    * @param update 更新值
     * @return true | false */
    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);

    上述中的偏移量是什么呢,我们来看一下:AtomicUnsafeUpdaterTest的实现中,静态字段valueOffset即为字段value的内存偏移地址,valueOffset的值在AtomicInteger初始化时,在静态代码块中通过Unsafe的objectFieldOffset方法获取。在AtomicInteger中提供的线程安全方法中,通过字段valueOffset的值可以定位到AtomicUnsafeUpdaterTest对象中value的内存地址,从而可以根据CAS实现对value字段的原子操作。

    下图为某个AtomicInteger对象自增操作前后的内存示意图,对象的基地址baseAddress=“0x110000”,通过baseAddress+valueOffset得到value的内存地valueAddress=“0x11000c”;然后通过CAS进行原子性的更新操作,成功则返回,否则继续重试,直到更新成功为止。

    2、unsafe线程调度

    包括线程挂起、恢复、锁机制等方法。

    //取消阻塞线程
    public native void unpark(Object thread);
    //阻塞线程
    public native void park(boolean isAbsolute, long time);
    //获得对象锁(可重入锁)
    @Deprecated
    public native void monitorEnter(Object o);
    //释放对象锁
    @Deprecated
    public native void monitorExit(Object o);
    //尝试获取对象锁
    @Deprecated
    public native boolean tryMonitorEnter(Object o);
    方法park、unpark即可实现线程的挂起与恢复,将一个线程进行挂起是通过park方法实现的,调用park方法后,线程将一直阻塞直到超时或者中断等条件出现;unpark可以终止一个挂起的线程,使其恢复正常。在使用park和unpark的时候是可以颠倒的,先使用unpark,相当于取得一张票,在使用park的时候相当于使用这张票。
    典型应用
    Java锁和同步器框架的核心类AbstractQueuedSynchronizer,就是通过调用LockSupport.park()和LockSupport.unpark()实现线程的阻塞和唤醒的,而LockSupport的park、unpark方法实际是调用Unsafe的park、unpark方式来实现。 
    public class ThreadParkerTest {
    
        public static void main(String[] args) {
    
            /*Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("thread - is running----");
                    LockSupport.park();//阻塞当前线程
                    System.out.println("thread is over-----");
                }
            });
    
            t.start();
    
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            LockSupport.unpark(t);//唤醒指定的线程*/
    
            //拿出票据使用
            LockSupport.park();
    
            System.out.println("main thread is over");
            //相当于先往池子里放了一张票据
            LockSupport.unpark(Thread.currentThread());//Pthread_mutex
    
            System.out.println("im running step 1");
    
        }
    
    }

    public class ObjectMonitorTest {
        static Object object = new Object();
    
    /*    public void method1(){
            unsafe.monitorEnter(object);
        }
    
        public void method2(){
            unsafe.monitorExit(object);
        }*/
    
        public static void main(String[] args) {
    
            /*synchronized (object){
            }*/
            Unsafe unsafe = UnsafeInstance.reflectGetUnsafe();
    
            unsafe.monitorEnter(object);//获取锁
            //业务逻辑写在此处之间
            unsafe.monitorExit(object);//锁释放
    
        }
    
    
     

    3、内存屏障

    在Java 8中引入,用于定义内存屏障(也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的
    所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。
    //内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前
    public native void loadFence();
    //内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前
    public native void storeFence();
    //内存屏障,禁止load、store操作重排序
    public native void fullFence();
    典型应用
    在Java 8中引入了一种锁的新机制——StampedLock,它可以看成是读写锁的一个改进版本。StampedLock提供了一种乐观读锁的实现,这种乐观读锁类似于无锁的操作,完
    全不会阻塞写线程获取写锁,从而缓解读多写少时写线程“饥饿”现象。由于StampedLock提供的乐观读锁不阻塞写线程获取读锁,当线程共享变量从主内存load到线程工作内存时,会存在数据不一致问题,所以当使用StampedLock的乐观读锁时,需要遵从如下图用例中使用的模式来确保数据的一致性。
    public class FenceTest {
    
        public static void main(String[] args) {
    
            UnsafeInstance.reflectGetUnsafe().loadFence();//读屏障
    
            UnsafeInstance.reflectGetUnsafe().storeFence();//写屏障
    
            UnsafeInstance.reflectGetUnsafe().fullFence();//读写屏障
    
        }
    }

     以上就是关于原子操作和Unsafe的解读,欢迎留言评论,谢谢。

  • 相关阅读:
    ubuntu9.04 解决关机beep声音
    『转』饯行:理想主义终结年代的七种兵器
    尼康数码单反DX Nikkor镜头介绍
    Nikkor镜头介绍
    [转]IDL中全局变量的处理
    APSC画幅
    开心时刻1
    C# 相对路径
    使用C#语言,从Excel2007中读取数据,并显示到Form中的DataGridView。
    C# 讀取Excel、xlsx文件Excel2007
  • 原文地址:https://www.cnblogs.com/yatou-blog/p/11683977.html
Copyright © 2011-2022 走看看