Atomic包简介及分类
java.util.concurrent
(一般简称juc
)包下的atomic包提供了一系列在并发场景下尽量无锁实现原子操作的类,其核心思想是使用CAS+循环实现轻量级乐观锁,在并发竞争不激烈的情况下效率会比加锁实现好很多。除Striped64
由JCP JSR-166 Expert Group成员协助完成外,作者都是Doug Lea大师。虽然juc包整体代码量不大但每次看都有新的思考和收获。
原子包装类:
- AtomicBoolean
- AtomicInteger
- AtomicLong
- AtomicReference
- AtomicIntegerArray
- AtomicLongArray
- AtomicReferenceArray
- AtomicStampedReference
- AtomicMarkableReference
属性更新类:
- AtomicIntegerFieldUpdater
- AtomicLongFieldUpdater
- AtomicReferenceFieldUpdater
高并发计算类:
- Striped64
- LongAdder
- DoubleAdder
- LongAccumulator
- DoubleAccumulator
介绍
原子包装类
一些通用属性:
serialVersionUID
序列化版本号unsafe
实现具体CAS操作的对象valueOffset
用于CAS操作的配置,记录的是value的具体地址,可理解为具体CAS操作的地址value
使用volatile修饰的实际值,对这个值的修改会对其他线程立即可见,这也就是atomic包存在的意义
一些通用方法:
get
获取当前值set
不考虑原值直接设置当前值compareAndSet
CAS设置当前值weakCompareAndSet
CAS设置当前值,JAVA 8中实际与compareAndSet结果完全相同,一些参考资料表示某些实现中weakCompareAndSet在compare失败时立刻返回falselazySet
延迟设置值,减少内存屏障,但设置的值并非立刻对其他线程可见,与atomic定义似乎相关性不大,一般用不到getAndSet
get+set
AtomicBoolean
提供对boolean的原子操作,内部使用int
作为底层操作类型而不是boolean
类型,参考资料认为由于跨平台特性,JAVA的Unsafe机制的最小操作类型是int
类型,即Unsafe类只提供了compareAndSwapObject
/compareAndSwapInt
/compareAndSwapLong
几种CAS操作。
AtomicInteger
提供对int的原子操作,getAndIncrement
等一系列自增/自减等方法依赖unsafe.getAndAddInt
方法实现,即底层都是compareAndSwapInt
循环操作,包括getAndUpdate
/getAndAccumulate
等方法也是如此。
AtomicLong
提供对long的原子操作,大体与AtomicInteger差不多,底层使用compareAndSwapLong
循环操作。
同时,判断了当前VM是否支持对long类型直行CAS,如果不支持可能在CAS时进行额外的加锁操作。
AtomicReference
同上,提供对Object引用的原子操作,底层使用compareAndSwapObject
循环操作。
AtomicIntegerArray
底层操作int[]
类型,通过计算数组元素的偏移量对对应的元素值进行原子操作(也只能对元素进行操作),这要求底层数组在内存中需要是物理连续的。由于int[]
对象内部元素并非volatile,即内部元素不是直接对其他线程具有可见性的,因此对于每个元素的操作是通过unsafe方法实现的如unsafe.getIntVolatile
,具体方式未知,猜测是通过加锁或者虚拟机层面支持的原子操作。
AtomicLongArray / AtomicReferenceArray
思路与AtomicIntegerArray相同。
AtomicStampedReference / AtomicMarkableReference
仍然是原子包装类,不过包装的对象由原本的单个值改为一组值:
AtomicStampedReference<V>
包装了一个Pair<V>
,可以理解为对二元组<V, int>
处理,对整个pair统一CAS处理,可以解决CAS的ABA问题AtomicMarkableReference
同上,二元组为<V, boolean>
AtomicFieldUpdater类
AtomicFieldUpdater将原子包装类的功能进行了拆分,将值的具体保存位置与原子操作分离。示例:
public class Test {
// 保存位置
private volatile int value;
// 更新位置,最好使用static final修饰
private static final AtomicIntegerFieldUpdater<Test> valueUpdater = AtomicIntegerFieldUpdater.newUpdater(Test.class, "value");
public void printValue() {
System.out.println(value);
}
public void incrementValue() {
//value++; // 显然不可以这么做
valueUpdater.incrementAndGet(this);
}
public static void main(String[] args) {
Test test = new Test();
test.printValue();
test.incrementValue();
test.printValue();
}
}
在分离后,value在并发取值等操作上可以如非包装类一样操作,但并发更新操作上应通过updater操作。
updater的原理:
- AtomicFieldUpdater类都是抽象类,CAS的具体方法(如
compareAndSet
)均为抽象方法,而内部类继承了这个抽象类并实现了其中的CAS相关方法 - AtomicFieldUpdater类提供的方法与原子包装类提供的方法一致
- 内部类通过反射获取真正需要操作的目标属性,构造方法签名以AtomicIntegerFieldUpdater为例:
AtomicIntegerFieldUpdaterImpl(final Class<T> tclass, final String fieldName, final Class<?> caller)
- 因为是通过反射实现的原子操作,updater需要进行很多验证操作,包括属性的可见性、类型检查等,尤其在每个原子操作前都进行了
accessCheck
确保输入的调用方对象(上述代码的test)与构造方法传入的类(Test)一致
根据原理我们可以知道,使用AtomicFieldUpdater需要遵循一些约定:
- 操作的属性需要使用volatile修饰,否则无法保证线程可见性
- 操作的属性需要对于updater可见,否则无法访问
- 操作的属性不能使用static修饰,因为updater通过反射操作的是实例的属性
- 操作的属性不能使用final修饰,否则无法更新
- 操作的属性与updater可操作的类型必须一致,如long类型对应AtomicLongFieldUpdater
- updater可操作类的指定对象,最好使用static final修饰,否则不如直接使用原子包装类
实际上使用updater操作和直接使用原子包装类的作用是一样的,完全可以实现相同的功能,而由于updater使用反射获取目标对象的属性且每次操作需要做前置检查,使得updater不光限制条件多,性能还不如直接使用原子包装类。那么我们为什么要用updater呢?参考资料给出了两种合理的使用场景:
- 多数情况下希望按照非包装类使用(即volatile修饰,可以直接使用 + - * / 连接),偶尔需要通过原子操作更新时
- 由于一个static修饰的updater实例可以对调用方(上述Test类)的所有对象起作用,使用原子包装类会比非包装类占用更多的内存,当存在大量调用方对象时尤其如此,这时候使用updater可以极大减少内存占用
AtomicIntegerFieldUpdater / AtomicLongFieldUpdater / AtomicReferenceFieldUpdater
分别对应int/long/Object的属性更新。
其中AtomicLongFieldUpdater
通过上述AtomicLong判断VM是否支持long类型CAS操作的结果,提供了两种updater:
if (AtomicLong.VM_SUPPORTS_LONG_CAS)
return new CASUpdater<U>(tclass, fieldName, caller);
else
return new LockedUpdater<U>(tclass, fieldName, caller);
CASUpdater通过unsafe中的CAS方法进行原子操作,而LockedUpdater通过synchronized
对updater对象加锁实现原子更新。
而AtomicReferenceFieldUpdater
除了accessCheck
对可见性检查外,还通过valueCheck
对null值和值的类型进行检查。
高并发计算类
之前写的Java并发累加器一文介绍过,这里直接copy一部分过来。
Striped64
内部包含子类Cell,实际上就是原子包装类功能的子集,只保存value及其CAS功能,但没有其他诸如get、incrementAndGet等功能。并且Cell类使用@Contended
注解避免伪共享问题。
内部定义了transient volatile long base
,用于“保存值的一部分”;还定义了transient volatile Cell[] cells
,用于“保存值的另一部分”。没错,base和cells共同组成了最终的值。
在并发程度较高时,原子包装类使用的CAS操作失败频率也较高,且多了很多不必要的资源消耗,导致性能下降。
Striped64考虑到原子包装类中CAS竞争的资源单一(都在竞争value资源),选择在有冲突时分散竞争资源,为每个线程分配一个Cell,让每个资源竞争对应的资源(cell),大幅减少冲突,在特定的高并发场景性能显著优于原子包装类。详情参见旧文。
LongAdder / DoubleAdder
继承Striped64
,功能上只是简单实现了累加器所需的方法,使用的是null二元计算,即使用默认的加法。详情参见旧文。
LongAccumulator / DoubleAccumulator
同上,不过使用的是自定义的二元计算方法。
参考资料
The Atomic classes in Java: atomic field updaters
本文搬自我的博客,欢迎参观!