• 原子操作类(一)原子操作类详细介绍


    引言

      Java从JDK1.5开始提供了java.util.concurrent.atomic包,方便程序员在多线程环境下,无锁的进行原子操作。原子变量的底层使用了处理器提供的原子指令,但是不同的CPU架构可能提供的原子指令不一样,也有可能需要某种形式的内部锁,所以该方法不能绝对保证线程不被阻塞。

      因为变量的类型有很多种,所以在Atomic包里一共提供了13个类,可分为4种类型的原子更新方式,分别是

    • 原子更新基本类型
    • 原子更新数组
    • 原子更新引用
    • 原子更新属性(字段)

    Atomic包里的类基本都是使用Unsafe实现的包装类。

    一、基本类型的原子操作类

    Atomic 包提供了以下3个基本类型的原子操作类:

    • AtomicBoolean: 原子更新布尔类型;
    • AtomicInteger: 原子更新整形;
    • AtomicLong: 原子更新长整形;

    以上3个类提供的方法几乎一模一样,所以本文仅以AtomicInteger为例,常用方法如下:

    • int addAndGet(int delta): 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果
    • boolean compareAndSet(int expect,int update): 如果输入的数值等于预期值,则以原子方式将该值设置为输入的值
    • int getAndIncrement(): 以原子方式将当前值加1,注意,这里返回的是自增前的值,相当于 (i++) 的形式。
    • void lazySet(int newValue): JDK6所增加,最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。关于该方法的更多信息可以参考并发编程网翻译的一篇文章《AtomicLong.lazySet是如何工作的?》,文章地址是“http://ifeve.com/howdoes-atomiclong-lazyset-work/”
    • int getAndSet(int newValue): 以原子方式设置为newValue的值,并返回旧值。

    @ Example1AtomicInteger 使用例子

    import java.util.concurrent.atomic.AtomicInteger;
    
    public class AtomicIntegerTest {
    
    	static AtomicInteger ai = new AtomicInteger(1);
    
    	public static void main(String[] args) {
    		System.out.println(ai.getAndIncrement());
    		System.out.println(ai.get());
    	}
    }
    

    运行结果:

    1
    2

    其余的基本类的原子操作类的解决方案

      java 的基本类型一共有8个,然而JDK却只提供了三个,如何原子的更新其他的基本类型呢?特别是常用的charfloatdouble。Atomic包里的类基本都是使用Unsafe实现的,让我们一起看一下Unsafe的源码.

     /**
         * 如果当前数值是expected,则原子的将Java变量更新成x
         * @return 如果更新成功则返回true
         */
        public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);
        
        public final native boolean compareAndSwapInt(Object o, long offset, int expected,int x);
    
        public final native boolean compareAndSwapLong(Object o, long offset, long expected, long x);
    
    

      通过源码,可以发现Unsafe只提供了3种CAS方法,那么 AtomicBoolean 是如何实现的呢,我们再来看一下 AtomicBoolean 的源码。只给出关键部分

    public class AtomicBoolean implements java.io.Serializable {
    //存储值:1,0;1代表 true,0 代表 false 
    private volatile int value;
    //Unsafe 对象
     private static final Unsafe unsafe = Unsafe.getUnsafe();
    
    
    public final boolean compareAndSet(boolean expect, boolean update) {
            //boolean 类型 转换成 整形,true 对应 1,false 对应 0;
            int e = expect ? 1 : 0;
            int u = update ? 1 : 0;
            return unsafe.compareAndSwapInt(this, valueOffset, e, u);
        }
    
    public final void set(boolean newValue) {
            value = newValue ? 1 : 0
        }
    
    public final boolean get() {
            return value != 0;
        }
    
    //.......
    }
    

      从AtomicBoolean 的源码可以看出,它是先把 boolean 的值转换成整型来存储,再使用compareAndSwapInt进行CAS。所以其他基本类型的原子类型也可参照这个思路来实现。

      思路有了,接下来考虑怎么实现。bytecharint 的转换很容易,他们的原子操作类就很容易实现了。那floatdouble 要怎样才能以整形或者长整形来存储值呢?
      Float 类中 提供了static两个方法:通过 Static int floatToIntBits(float value) (根据 IEEE 754 浮点“单一格式”位布局,返回指定浮点值的表示形式)方法,便可以将float 的值以整形的方式存储下来。如果要取值,则通过 static float intBitsToFloat(int bits)方法,将存储下来的整形的浮点值转换回原来的float值。
      同理,Double 类也提供了类似的两个方法:static long doubleToLongBits(double value) 产生long类型的浮点值。static double longBitsToDouble(long bits)long的浮点值转换回double值。

    下面是 AtomicDouble 的实现例子:

    @ Example2AtomicDouble 的实现例子

    import java.util.concurrent.atomic.AtomicLong;
    
    public class AtomicDouble extends Number {
    
    	private AtomicLong bits;
    	
    	public AtomicDouble() {
    		this(0d);
    	}
    	
    	public AtomicDouble(Double value){
    		bits = new AtomicLong(Double.doubleToLongBits(value));
    	}
    
    	public final double get() {
    		return Double.longBitsToDouble(bits.get());
    	}
    
    	public final void set(double value) {
    		bits.set(Double.doubleToLongBits(value));
    	}
    
    	public final double addAndGet(double delta) {
    		long newBits = bits.addAndGet(Double.doubleToLongBits(delta));
    		return Double.longBitsToDouble(newBits);
    	}
    	
    	public final double getAndAdd(double delta) {
    		long oldBits = bits.getAndAdd(Double.doubleToLongBits(delta));
    		return Double.longBitsToDouble(oldBits);
    	}
    
    	public final double getAndSet(double newValue) {
    		long oldBits = bits.getAndSet(Double.doubleToLongBits(newValue));
    		return Double.longBitsToDouble(oldBits);
    	}
    
    	public final boolean compareAndSet(double expect, double update) {
    		return bits.compareAndSet(Double.doubleToLongBits(expect), Double.doubleToLongBits(update));
    	}
    
    	public final boolean weakCompareAndSet(double expect, double update) {
    		return bits.weakCompareAndSet(Double.doubleToLongBits(expect), Double.doubleToLongBits(update));
    	}
    	
    	public final void lazySet(double newValue) {
    		 bits.lazySet(Double.doubleToLongBits(newValue));
    	}
    
    	@Override
    	public int intValue() { return (int) this.get(); }
    	@Override
    	public long longValue() { return (long) this.get(); }
    	@Override
    	public float floatValue() { return (float) this.get();}
    	@Override
    	public double doubleValue() { return this.get(); }
    }
    

    关于原子操作类实现其余猜想:

      除了上面的那种实现方案外,在 stackOverflow 上也发现两个原子操作类的实现方案:

    1. 利用 AtomicReference 类;如 Float 的原子操作类可以是 AtomicReference<Float>。不可,经过测试,这种方法是不行的,因为像compareAndSet这类方法比较的是对象的内存地址,而不会使用equal()方法进行比较。
    2. 使用JDK1.8新增的方法:DoubleAdderDoubleAccumulator。没去深入了解,粗略看了下,这两个类都是适合于“多写少读”的情况。

    二、数组的原子操作类

    通过原子的方式更新数组里的某个元素,Atomic包提供了以下3个类:

    • AtomicIntegerArray: 原子更新整型数组里的元素
    • AtomicLongArray: 原子更新长整型数组里的元素
    • AtomicReferenceArray: 原子更新引用类型数组里的元素

    这几个类的方法几乎一样,以AtomicIntegerArray为例,AtomicIntegerArray类主要是提供原子的方式更新数组里的整型;

    构造方法:
    AtomicIntegerArray(int length): 创建给定长度的新 AtomicIntegerArray。
    AtomicIntegerArray(int[] array): 创建与给定数组具有相同长度的新 AtomicIntegerArray,并从给定数组复制其所有元素。

    其常用方法:
    int addAndGet(int i, int delta): 以原子方式将输入值与数组中索引i的元素相加。
    boolean compareAndSet(int i, int expect, int update): 如果当前值等于预期值,则以原子方式将数组位置i的元素设置成update值。

    @ Example3AtomicIntegerArray 的使用例子

    public class AtomicIntegerArrayTest {
    
    	static int[] value = new int[] { 1, 2 };
    
    	static AtomicIntegerArray ai = new AtomicIntegerArray(value);
    
    	public static void main(String[] args) {
    		ai.getAndSet(0, 3);
    		System.out.println(ai.get(0));
                    System.out.println(value[0]);
    	}
    }
    

    运行结果:

    3
    1

    AtomicIntegerArray类需要注意的是 ,数组value通过构造方法传递进去,然后AtomicIntegerArray会将当前数组复制一份,所以当AtomicIntegerArray对内部的数组元素进行修改时,不会影响到传入的数组。


    三、引用类型的原子操作类

     基本类型的原子操作类只能更新基本类型的值,不能更新引用类型的对象引用。Atomic包提供了以下三个类,可以原子方式更新的对象引用。

    • AtomicReference: 原子更新引用类型。
    • AtomicStampedReference: 原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更数据和数据的版本号,可以解决使用CAS进行原子更新时,可能出现的ABA问题。
    • AtomicMarkableReference: 原子更新带有标记位的引用类型。可以原子的更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef, boolean initialMark)

    1. AtomicReference 介绍

    构造方法

    AtomicReference(): 使用 null 初始值创建新的 AtomicReference
    AtomicReference(V initialValue): 使用给定的初始值创建新的 AtomicReference

    常用的方法: 与前面介绍的类差不多,get()set(V newValue)compareAndSet(V expect, V update)getAndSet(V newValue);

    @ Example4AtomicReference 的使用例子

    public class AtomicReferenceTest {
    
    	public static AtomicReference<user> atomicUserRef = new AtomicReference</user><user>();
    
    	public static void main(String[] args) {
    		User user = new User("conan", 15);
    		atomicUserRef.set(user);
    		User updateUser = new User("Shinichi", 17);
    		atomicUserRef.compareAndSet(user, updateUser);
    		System.out.println(atomicUserRef.get().getName());
    		System.out.println(atomicUserRef.get().getOld());
    	}
    
    	static class User {
    		private String name;
    		private int old;
    
    		public User(String name, int old) {
    			this.name = name;
    			this.old = old;
    		}
    
    		public String getName() {
    			return name;
    		}
    
    		public int getOld() {
    			return old;
    		}
    	}
    }
    

    运行结果:

    Shinichi
    17

    2. AtomicMarkableReference 与 AtomicStampedReference 介绍

      这两个类的作用与 AtomicReference 的作用非常相似,都是原子更新引用类型。但他们还有一个作用:能解决CAS过程中的ABA问题

    什么是ABA问题?

      ABA的问题就是:在多线程的环境下,线程1将A的值赋给了B,然后B的值又重新赋值了A。在这个过程中,A已经被修改了一次了,但是线程2不知道,在进行CAS时,认为A的值没有被修改过,所以就进行修改。当然,如果只对结果敏感,而对修改的次数不敏感,那么这个问题就无所谓了。

    AtomicStampedReference 和 AtomicMarkableReference 是怎么解决ABA问题的?

      这两个类的解决方案也很简答,就是多维护了一个标志位记录修改的状态 或者 维护一个版本号记录修改的次数,然后进行CAS时,也会比较标志位或者版本号。简单看一下源码吧。

    AtomicMarkableReference 用的是标志位mark(布尔类型)来标志修改的状态,下面是关键部分源码:

    //构造方法,initialRef 是 初始的引用类型,initialMark 是初始的标志位
    public AtomicMarkableReference(V initialRef,  boolean initialMark);
    
    //CAS 不仅要比较引用类型,还要比较标志位
    public boolean compareAndSet(V expectedReference, V newReference, boolean expectedMark, boolean newMark) {
            Pair<V> current = pair;
            return
                expectedReference == current.reference &&
                expectedMark == current.mark &&
                ((newReference == current.reference &&
                  newMark == current.mark) ||
                 casPair(current, Pair.of(newReference, newMark)));
     }
    

    AtomicStampedReference 用的则是版本号stamp(整形int) 来记录修改的次数,下面是关键部分的源代码:

    //构造方法,初始引用类型 和 初始版本号
     public AtomicStampedReference(V initialRef, int initialStamp);
    
    //CAS 不仅要比较引用类型,还要比较版本号
      public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) {
            Pair<V> current = pair;
            return
                expectedReference == current.reference &&
                expectedStamp == current.stamp &&
                ((newReference == current.reference &&
                  newStamp == current.stamp) ||
                 casPair(current, Pair.of(newReference, newStamp)));
        }
    

    @ Example5AtomicStampedReference 的解决ABA问题的例子

    看一个AtomicStampedReference的例子。当需要执行一次更新当前用户的操作,这里故意将更新的用户就是当前用户,也就是更新后对象引用是没有变化的。线程A、线程B 都接到这个任务,但只能执行一次,意味着得有一个线程更新失败。User类的代码请查看上一个例子。

    public class AtomicStampedReferenceTest {
    
    	public static void main(String[] args) throws InterruptedException {
    		User user = new User("小赫",25);
    		//初始版本为1
    		int stamp = 1;
            
    		//设置当前用户是  小赫
    		AtomicStampedReference<User> currentAtomicUser = new AtomicStampedReference<User>(user, stamp);
    		
    		//更新当前用户,更新的目标用户就是当前用户
    		UpdateUserTask task = new UpdateUserTask(stamp, currentAtomicUser, user, user);
    		//线程A,线程B执行相同的更新任务,但更新操作只需要执行一次。
    		Thread threadA = new Thread(task,"ThreadA");
    		Thread threadB = new Thread(task,"ThreadB");
    		threadA.start();
    		threadB.start();
        }
    }
    
    class UpdateUserTask implements Runnable{
    
    	private int stamp;
    	private AtomicStampedReference<User> currentAtomicUser;
    	private User newUser;
    	private User oldUser;
    	
    	public UpdateUserTask(int stamp, AtomicStampedReference<User> currentAtomicUser,User newUser,User oldUser) {
    		this.stamp = stamp;
    		this.currentAtomicUser = currentAtomicUser;
    		this.newUser = newUser;
    		this.oldUser = oldUser;
    	}
    
    	@Override
    	public void run() {
    		//更新当前用户,版本号加一
    		boolean b = currentAtomicUser.compareAndSet(oldUser, newUser, stamp, stamp+1);
    		if(b){//更新执行成功
    			System.out.println("线程"+Thread.currentThread().getName()+": 成功执行更新操作");
    		}else{//更新失败
    			System.out.println("线程"+Thread.currentThread().getName()+": 当前用户是 "+ currentAtomicUser.getReference().getName()+" ,版本号已经过期,更新操作已经被其他线程完成");
    		}
    	}
    	 
     }
    

    运行结果:

    线程ThreadB: 当前用户是 小赫 ,版本号已经过期,更新操作已经被其他线程完成
    线程ThreadA: 成功执行更新操作

      从结果可以看出,当线程B执行任务时,尽管当前用户的对象引用没有改变,但版本号却已经改变了,线程B从而知道了更新操作已经被执行了,于是便不再执行更新。


    四、 更新字段的原子操作类

    如果我们只需要某个类里的某个字段,那么就需要使用原子更新字段类,Atomic包提供了以下三个类:

    • AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。通过静态工厂方法创建newUpdater(Class<U> tclass, String fieldName)
    • AtomicLongFieldUpdater: 原子更新长整型字段的更新器。通过静态工厂方法创建newUpdater(Class<U> tclass, String fieldName)
    • AtomicReferenceFieldUpdater 原子更新引用类型里的字段。比前两个方法的范围要广,可以更新任意类型的字段,通过静态的工厂方法创建 newUpdater(Class<U> tclass, Class<W> vclass, String fieldName)

    原子更新字段类都是抽象类,每次使用都时候必须使用静态方法newUpdater创建一个更新器。原子更新类的字段的必须使用public volatile修饰符。以上3个类提供的方法差不多,下面给出AtomicIntegerFieldUpdater 的例子:

    @ Example6AtomicIntegerFieldUpdater 例子

    public class AtomicIntegerFieldUpdaterTest {
    
    	private static AtomicIntegerFieldUpdater<User> a = AtomicIntegerFieldUpdater
    			.newUpdater(User.class, "old");
    
    	public static void main(String[] args) {
    		User conan = new User("conan", 10);
    		System.out.println(a.getAndIncrement(conan));
    		System.out.println(a.get(conan));
    	}
    
    	public static class User {
    		private String name;
    		public volatile int old;
    
    		public User(String name, int old) {
    			this.name = name;
    			this.old = old;
    		}
    
    		public String getName() {
    			return name;
    		}
    
    		public int getOld() {
    			return old;
    		}
    	}
    }
    

    运行结果:

    10
    11

  • 相关阅读:
    centos7 centos-home 磁盘空间转移至centos-root下
    CACTI优化-流量接口统计total输入和输出流量数据
    shell管理前台进程
    Rancher中ConfigMap使用实例
    Rancher调试微服务
    ssh配置免密登录异常处理
    漏洞复现:ActiveMQ任意文件写入漏洞(CVE-2016-3088)
    ubuntu更新源
    Vulnhub实战靶场:CH4INRULZ: 1.0.1
    CVE-2019-15107 Webmin远程命令执行漏洞复现
  • 原文地址:https://www.cnblogs.com/jinggod/p/8495395.html
Copyright © 2020-2023  润新知