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

引言

  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

posted @ 2018-03-02 23:05  jinggod  阅读(1522)  评论(0编辑  收藏  举报