26.原子类操作类
Posted 纵横千里,捭阖四方
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了26.原子类操作类相关的知识,希望对你有一定的参考价值。
JDK1.5之前,为了保证Java中对单个变量的多个独立操作的原子性和安全性,通常会使用到synchronized锁,但是synchronized需要底层操作系统mutex资源的支持,这是一种重量级资源,性能比较低!
JDK1.5的时候,新增了JUC包,增加了许多和同步有关的特性,大大提高了使用Java进行并发编程的效率,比如并发集合、并发队列、新lock锁等。另外,JUC包下面还提供了一个java.util.concurrent.atomic子包,这个atomic包中的类用于在多线程环境下实现单个变量多个独立操作(比如读-写)的连续原子性,并且都比较高效,因为它们都是由基于偏移量(类似于指针)的非阻塞CAS算法实现,用于替代锁的使用。
JDK1.8的atomic包中具有17个原子类,根据支持的更新变量的类型,我们可以对常用原子类分为三种,分别是原子更新单个变量、原子更新数组、原子更新引用属性(字段)。 atomic 包下的常用原子类如下:
类 | 摘要 |
---|---|
AtomicBoolean | 用原子方式更新的 boolean 值。 |
AtomicInteger | 用原子方式更新的 int 值。 |
AtomicLong | 用原子方式更新的 long 值。 |
AtomicReference< V > | 用原子方式更新的对象引用。 |
AtomicMarkableReference< V > | 维护带有boolean标志位的对象引用,可以原子方式对其进行更新。 |
AtomicStampedReference< V > | 维护带有int整数版本号的对象引用,可用原子方式对其进行更新。 |
AtomicIntegerArray | 用原子方式更新其元素的 int 数组。 |
AtomicLongArray | 用原子方式更新其元素的 long 数组。 |
AtomicReferenceArray< E > | 用原子方式更新其元素的对象引用数组。 |
AtomicIntegerFieldUpdater< T > | 基于反射的实用工具,可以对指定类的指定非私有非静态的 volatile int 字段进行原子更新。 |
AtomicLongFieldUpdater< T > | 基于反射的实用工具,可以对指定类的指定非私有非静态的 volatile long 字段进行原子更新。 |
AtomicReferenceFieldUpdater< T,V > | 基于反射的实用工具,可以对指定类的指定非私有非静态的 volatile 引用字段进行原子更新 。 |
LongAdder | JDK1.8新增加的原子类累加器,使用热点数据分离的思想对long数据进行加法运算,性能更佳! |
LongAccumulator | JDK1.8新增加的原子类累加器,使用热点数据分离的思想对long数据进行指定规则的运算,性能更佳! |
DoubleAdder | JDK1.8新增加的原子类累加器,使用热点数据分离的思想对double数据进行加法运算,性能更佳! |
DoubleAccumulator | JDK1.8新增加的原子类累加器,使用热点数据分离的思想对double数据进行指定规则的运算,性能更佳! |
1 原子更新基本类型类
使用原子的方式更新基本类型,Atomic包提供了以下3个类。
-
AtomicBoolean:原子更新布尔类型。
-
AtomicInteger:原子更新整型。
-
AtomicLong:原子更新长整型。
以上3个类提供的方法几乎一模一样,所以本节仅以AtomicInteger为例进行讲解, 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的值,并返回旧值。
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());
那么getAndIncrement是如何实现原子操作的呢?让我们一起分析其实现原理,getAndIncrement的源码如下所示:
public final int getAndIncrement()
return unsafe.getAndAddInt(this, valueOffset, 1);
继续看unsafe的实现:
public final int getAndAddInt(Object var1, long var2, int var4)
int var5;
do
var5 = this.getIntVolatile(var1, var2);
while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
可以看到这里的本质就是自旋锁,compareAndSwapInt()方法就是Unsafe提供的native那类型的原子更新操作。
实际上Java中atomic包下的原子类的基石就是:volatile字段修饰符+CAS算法(Unsafe提供)。前面已经对这两个技术进行了深入的讲解。
通过原子的方式更新单个变量,Atomic包提供了以下4个基础类:
-
AtomicBoolean:用原子方式更新的 boolean 值。
-
AtomicInteger:用原子方式更新的 int 值。
-
AtomicLong:用原子方式更新的 long 值。
-
AtomicReference< V >:用原子方式更新的对象引用。
Atomic包实际上只提供了3种基本类型的原子更新:int、long、boolean,其中boolean也是转换为int的0、1进行更新的,实际上并没有char、float和double等的CAS操作,实际上char、 float、double都可以转换为int或者long在进行操作,如果DoubleAdder就是采用Double.doubleToRawLongBits将double转换为long类型的值在进行操作。
/*Unsafe只提供了3种CAS方法.*/
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);
/*AtomicBoolean源码中,它是先把Boolean转换成int类型,再使用compareAndSwapInt进行CAS操作*/
public final boolean compareAndSet(boolean expect, boolean update)
int e = expect ? 1 : 0;
int u = update ? 1 : 0;
return unsafe.compareAndSwapInt(this, valueOffset, e, u);
上面四个原子类的原理几乎一致,我们后面主要以AtomicInteger来讲解。
我们看一下Atomiclnteger的源码,其内部有个value属性,它代表了Atomiclnteger 的当前实际取值,所有的方法都是围绕该值进行的。
private volatile int value;
public AtomicInteger(int initialValue)
value = initialValue;
public AtomicInteger()
/**
* 内部实际上依赖于Unsafe类的方法,堆value值进行操作
*/
private static final Unsafe unsafe = Unsafe.getUnsafe();
/**
* value字段的偏移量
*/
private static final long valueOffset;
static
try
//初始化value字段的偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
catch (Exception ex)
throw new Error(ex);
我们继续看一下get与set相关的几个操作:
public final int get()
return value;
public final void set(int newValue)
value = newValue;
/**
* 原子性的将当前值设为给定新值,返回旧值
* @param newValue 新值
* @return 旧值
*/
public final int getAndSet(int newValue)
return unsafe.getAndSetInt(this, valueOffset, newValue);
/**
* 如果当前值等于预期值,则以原子方式将该值设置为给定的新值
* @param expect 预期值
* @param update the new value
* @return true 更新成功 false 更新失败
*/
public final boolean compareAndSet(int expect, int update)
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
/**
* 原子性的将当前值加1,返回旧值
*
* @return 旧值
*/
public final int getAndIncrement()
return unsafe.getAndAddInt(this, valueOffset, 1);
/**
* 原子性的将当前值减1,返回旧值
*
* @return 返回旧值
*/
public final int getAndDecrement()
return unsafe.getAndAddInt(this, valueOffset, -1);
/**
* 原子性的将当前值增加delta,返回旧值
*
* @param delta 增加的值
* @return 旧值
*/
public final int getAndAdd(int delta)
return unsafe.getAndAddInt(this, valueOffset, delta);
/**
* 原子性的将当前值加1,返回新值
*
* @return 更新后的值
*/
public final int incrementAndGet()
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
/**
* 原子性的将当前值减1,返回新值
*
* @return 更新后的值
*/
public final int decrementAndGet()
return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
/**
* 原子性的将当前值增加delta,返回新值
*
* @param delta 增加的值
* @return 更新后的值
*/
public final int addAndGet(int delta)
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
/**
1. 最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
2. 关于该方法的更多信息可以参考并发编程网翻译的一篇文章《AtomicLong.lazySet是如何工作的?》,文章地址是“http://ifeve.com/how-does-atomiclong-lazyset-work/”。
3. 4. @param newValue 新值
*/
public final void lazySet(int newValue)
unsafe.putOrderedInt(this, valueOffset, newValue);
2 带版本号的原子类
2.1 原理解析
通过原子的方式更新单个变量的原子类的升级版,Atomic包提供了以下2个类:
-
AtomicMarkableReference< V >:维护带有标记位的对象引用,可以原子方式对其进行更新。
-
AtomicStampedReference< V >:维护带有整数标志的对象引用,可用原子方式对其进行更新。
上面两个原子类的方法以及原理几乎一致,属于带有版本号的原子类。我们知道CAS操作的三大问题之一就是“ABA”问题: CAS需要再操作值的时候,检查值有没有发生变化,如果没有发生变化则更新。但是一个值,如果原来为A,变成了B,又变成了A,那么使用CAS进行compare and set的时候,会发现它的值根本没变化过,但实际上是变化过的。 ABA问题的解决思路就是使用版本号,1A->2B->3A,在Atomic包中,提供了一个现成的AtomicStampedReference类来解决ABA问题,使用的就是添加版本号的方法。还有一个AtomicMarkableReference实现类,它比AtomicStampedReference更加简单,AtomicStampedReference中每更新一次数据版本号也会更新一次,这样可以使用版本号统计到底更新了多少次,而AtomicMarkableReference仅仅使用了一个boolean值来表示值是否改变过,因此使用的比较少。 这里我们以AtomicStampedReference来讲解。
AtomicStampedReference内部不仅维护了我们的传递的对象reference,还维护了一个int类型的版本号stamp,它们都被存放到一个Pair类型的内部类实例中。当AtomicStampedReference 对应的数据被修改时,除了更新数据本身外,还必须要更新版本号,这个版本号一般都是自增的。当AtomicStampedReference 设置对象值时,对象值及版本号都必须满足期望值,才会更新成功。
/**
* Pair内部类,用于维护reference和stamp
*
* @param <T>
*/
private static class Pair<T>
/**
* 真正的数据
*/
final T reference;
/**
* 版本号
*/
final int stamp;
private Pair(T reference, int stamp)
this.reference = reference;
this.stamp = stamp;
/**
* 返回Pair实例
*/
static <T> Pair<T> of(T reference, int stamp)
return new Pair<T>(reference, stamp);
/**
* 由于要维护两个属性,因此干脆使用一个内部类对象来维护这两个属性
*/
private volatile Pair<V> pair;
/**
* 创建具有给定初始值的新 AtomicStampedReference。
*
* @param initialRef 初始值
* @param initialStamp 初始版本号
*/
public AtomicStampedReference(V initialRef, int initialStamp)
//初始化一个Pair对象,并初始化属性值
pair = Pair.of(initialRef, initialStamp);
在该类中,最重要的就是compareAndSet方法,它需要传递:期望值、新值、期望版本号、新版本号,当期望值和期望版本号都与此时内部的真实值和真实版本号相等的时候,就会调用compareAndSwapObject使用一个新的Pair对象替换旧的Pair对象,同时完成reference和stamp的更新。
/**
* 如果当前引用 == 预期引用,并且当前版本号等于预期版本号,则以原子方式将该引用和该标志的值设置为给定的更新值。
*
* @param expectedReference 预期引用
* @param newReference 新引用
* @param expectedStamp 预期版本号
* @param newStamp 新版本号
* @return 如果成功,则返回 true
*/
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp)
Pair<V> current = pair;
//一系列的判断,如果两个预期值都相等,那么尝试调用compareAndSwapObject使用新的Pair对象替代旧的Pair对象
//这样就同时完成了reference和stamp的更新
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
/**
* CAS替换内部的Pair对象的方法
*
* @param cmp 预期pair对象
* @param val 新pair对象
* @return 如果成功,则返回 true
*/
private boolean casPair(Pair<V> cmp, Pair<V> val)
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
/**
* @return 获得当前保存的对象引用
*/
public V getReference()
return pair.reference;
/**
* @return 获得当前保存的版本号
*/
public int getStamp()
return pair.stamp;
/**
* 设置新对象引用和版本号
*
* @param newReference 新对象引用
* @param newStamp 新版本号
*/
public void set(V newReference, int newStamp)
Pair<V> current = pair;
//如果新对象引用以及新版本号和之前的都一样那就不设置
//否则就是新建一个Pair对象并设置相应的属性,替代原来的Pair对象
if (newReference != current.reference || newStamp != current.stamp)
this.pair = Pair.of(newReference, newStamp);
2.2 案例分析
实际上,如果更新的数据是无状态的数据,那么使用基本的原子类也可以完成目的,即如果线程A将值从1->2->1,而线程B仅仅是使用了值,这是没什么问题的,但是如果和业务相关联,比较的对象是有状态的,那么可能会出现严重问题。 比如还是线程A将值从1->2->1,而线程B的业务逻辑是如果发现数据改变过,那么就不能操作,这样的话就不能单纯的比较值了,这就需要用到版本号了。
public class AtomicStampedReferenceDemo
public static void main(String args[])
//初始值为0,版本号为0
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(0, 0);
Thread thread = new Thread(() ->
//先获取标志位
int timestamp = atomicStampedReference.getStamp();
//获取原值
int reference = atomicStampedReference.getReference();
System.out.println("原值reference: " + reference);
//阻塞,等待被唤醒
LockSupport.park();
if (atomicStampedReference.compareAndSet(reference, reference + 1, timestamp, timestamp + 1))
System.out.println("更新成功,新值reference: " + atomicStampedReference.getReference());
else
System.out.println("更新失败,新值reference: " + atomicStampedReference.getReference());
System.out.println("虽然原值和新值相等,但是是在线程阻塞过程中值发生了变化,变化了" + atomicStampedReference.getStamp() + "次");
);
thread.start();
Thread thread1 = new Thread(() ->
//对数据先加一再减一,反复4次,最终reference的值是不变的
for (int i = 0; i < 4; i++)
int timestamp = atomicStampedReference.getStamp();
int reference = atomicStampedReference.getReference();
if (i % 2 == 0)
atomicStampedReference.compareAndSet(reference, reference + 1, timestamp, timestamp + 1);
else
atomicStampedReference.compareAndSet(reference, reference - 1, timestamp, timestamp + 1);
//唤醒阻塞的thread线程
LockSupport.unpark(thread);
);
thread1.start();
同样的逻辑,使用普通原子类就能更新成功:
public class AtomicRefrenceDemo
public static void main(String args[])
//初始值为0
AtomicReference<Integer> atomicReference = new AtomicReference<Integer>(0);
Thread thread = new Thread(() ->
int reference = atomicReference.get();
System.out.println("原值reference: " + reference);
//阻塞,等待被唤醒
LockSupport.park();
if (atomicReference.compareAndSet(reference, reference + 1))
System.out.println("更新成功,新值reference: " + atomicReference.get());
else
System.out.println("更新失败,新值reference: " + atomicReference.get());
);
thread.start();
Thread thread1 = new Thread(() ->
//对数据先加一再减一,反复4次,最终的值是不变的
for (int i = 0; i < 4; i++)
int reference = atomicReference.get();
if (i % 2 == 0)
atomicReference.compareAndSet(reference, reference + 1);
else
atomicReference.compareAndSet(reference, reference - 1);
//唤醒阻塞的thread线程
LockSupport.unpark(thread);
);
thread1.start();
3 原子更新数组
通过原子的方式更新数组里的某个元素,Atomic包提供了以下4个类。
-
AtomicIntegerArray:原子更新整型数组里的元素。
-
AtomicLongArray:原子更新长整型数组里的元素。
-
AtomicReferenceArray:原子更新引用类型数组里的元素。
-
AtomicIntegerArray类主要是提供原子的方式更新数组里的整型,其常用方法如下。
-
int addAndGet(int i,int delta):以原子方式将输入值与数组中索引i的元素相加。
-
boolean compareAndSet(int i,int expect,int update):如果当前值等于预期值,则以原子方式将数组位置i的元素设置成update值。
-
以上几个类提供的方法几乎一样,所以本节仅以AtomicIntegerArray为例进行讲解。
AtomicIntegerArray的使用实例代码
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]);
接下来我们看一下源码,可以看到内部就是一个int的数组,然后调用Unsafe的方法对数组的元素进行操作。
/**
* 使用Unsafe操作数组
*/
private static final Unsafe unsafe = Unsafe.getUnsafe();
/**
* 返回数组类型的第一个元素的偏移地址(基础偏移地址)。
* 如果arrayIndexScale方法返回的比例因子不为0,你可以通过结合基础偏移地址和比例因子访问数组的所有元素。
*/
private static final int base = unsafe.arrayBaseOffset(int[].class);
/**
* scale最高位的1的所在位数(从左从0开始),在计算某个索引的偏移量的时候
* 使用是该值进行位运算而不是scale进行传统乘法运算,提升效率
*/
private static final int shift;
/**
* 底层int数组
*/
private final int[] array;
static
//返回数组单个元素的大小,数组中的元素的地址是连续的,64位虚拟机应该是4
int scale = unsafe.arrayIndexScale(int[].class);
//大小必须是2的幂次方
if ((scale & (scale - 1)) != 0)
throw new Error("data type scale not a power of two");
//numberOfLeadingZeros用于返回scale的最高非零位前面的0的个数,包括符号位在内;
//31减去scale的最高非零位前面的0的个数,就表示scale最高位的1的所在位数,比如scale为2,那么shift为1,如果scale为4,那么shift为2
shift = 31 - Integer.numberOfLeadingZeros(scale);
/**
* 某个数组索引位置的元素的偏移量
* @param i 数组索引
* @return 该索引的偏移量
*/
private long checkedByteOffset(int i)
if (i < 0 || i >= array.length)
throw new IndexOutOfBoundsException("index " + i);
return byteOffset(i);
/**
* @param i 索引位置
* @return 返回某个数组索引位置的元素的偏移量
*/
private static long byteOffset(int i)
//这里就能明白shift的作用了,对于2的幂次方的scale:
//这里可以使用scale的最高为1的位置shift的位运算i << shift,代替scale*i的传统运算,效率提高
//比如scale=4,那么shift=2,如果i=3,那么i<<shift = 3 << 2 = 12 就等于 scale*i = 4 * 3 = 12
//比如scale=8,那么shift=3,如果i=3,那么i<<shift = 3 << 3 = 24 就等于 scale*i = 8 * 3 = 24
return ((long) i << shift) + base;
/**
* 创建给定长度的新 AtomicIntegerArray。
* @param length 给定长度
*/
public AtomicIntegerArray(int length)
array = new int[length];
/**
* 创建与给定数组具有相同长度的新 AtomicIntegerArray,并从给定数组复制其所有元素。
*
* @param array 给定数组
* @throws NullPointerException 如果数组为 null
*/
public AtomicIntegerArray(int[] array)
// 克隆数组,元素浅克隆
this.array = array.clone();
其常用方法如下,基于Unsafe的volatile和CAS操作:
/**
* 获取i索引位置的当前值
* @param i 多赢
* @return 当前值
*/
public final int get(int i)
return getRaw(checkedByteOffset(i));
private int getRaw(long offset)
//volatile的获取最新值
return unsafe.getIntVolatile(array, offset);
/**
* 在i索引位置设定为指定新值
* @param i 索引
* @param newValue 新值
*/
public final void set(int i, int newValue)
//volatile的写
unsafe.putIntVolatile(array, checkedByteOffset(i), newValue);
/**
* 以原子方式将元素设置在i索引位置,并返回旧值
* @param i 索引
* @param newValue 新值
* @return 旧值
*/
public final int getAndSet(int i, int newValue)
return unsafe.getAndSetInt(array, checkedByteOffset(i), newValue);
/**
* 以原子方式将输入值与数组中索引i的元素相加,并返回旧值
* @param i 索引
* @param delta 相加的数据
* @return 旧值
*/
public final int getAndAdd(int i, int delta)
return unsafe.getAndAddInt(array, checkedByteOffset(i), delta);
/**
* 以原子方式将输入值与数组中索引i的元素相加,并返回新值
* @param i 索引
* @param delta 相加的数据
* @return 更新后的值
*/
public final int addAndGet(int i, int delta)
return getAndAdd(i, delta) + delta;
/**
1. 如果当前值等于预期值,则以原子方式将数组位置i的元素设置成新值。
2. 3. @param i 索引
3. @param expect 预期值
4. @param update 新值
5. @return true表示CAS成功 false 表示CAS失败
*/
public final boolean compareAndSet(int i, int expect, int update)
return compareAndSetRaw(checkedByteOffset(i), expect, update);
4 原子更新引用类型
原子更新基本类型的AtomicInteger,只能更新一个变量,如果要原子更新多个变量,就需要使用这个原子更新引用类型提供的类。Atomic包提供了以下3个类。
-
AtomicReference:原子更新引用类型。
-
AtomicReferenceFieldUpdater:原子更新引用类型里的字段。
-
AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类 型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef,boolean initialMark)。
以上几个类提供的方法几乎一样,所以本节仅以AtomicReference为例进行讲解。AtomicIntegerFieldUpdater实际上是一个抽象类,它的实现类实际上在它的内部而且是私有的,因此只能使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性字符串名。 另外,这里对于对象的字段的设置是先采用getDeclaredField方法反射获取的对应字段的Filed对象,然后在对Filed对象进行操作,并且没有设置setAccessible权限,因此类的字段属性不能是私有属性! 由于CAS 操作会通过对象实例中的偏移量直接进行赋值,即Unsafe. objectFieldOffset()方法。因此,它不支持对static属性的赋值。 对象的字段还应该被设置为volatile类型,这样就能获取到最新的值。
AtomicReference的使用示例代码如下
public class AtomicFieldUpdaterTest
public static void main(String[] args)
AtomicIntegerFieldUpdater<User> old = AtomicIntegerFieldUpdater.newUpdater(User.class, "old");
AtomicReferenceFieldUpdater<User, String> name = AtomicReferenceFieldUpdater.newUpdater(User.class, String.class, "name");
User user = new User("user", 10);
System.out.println(old.getAndIncrement(user));
System.out.println(old.get(user));
System.out.println(name.getAndSet(user, "user2"));
System.out.println(name.get(user));
public static class User
volatile String name;
volatile int old;
User(String name, int old)
this.name = name;
this.old = old;
public String getName()
return name;
public int getOld()
return old;
代码中首先构建一个user对象,然后把user对象设置进AtomicReferenc中,最后调用 compareAndSet方法进行原子更新操作,实现原理同AtomicInteger里的compareAndSet方法。
以上是关于26.原子类操作类的主要内容,如果未能解决你的问题,请参考以下文章