1. 简介
Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。所以,所谓原子类说简单点就是具有原子操作特征的类
JUC 包的原子类都存放在 JUC 的 atomic 子包下。根据操作的数据类型,可以将 JUC 包中的原子类分为 4 类:
-
基本类型
- AtomicBoolean:原子更新布尔类型
- AtomicInteger:原子更新整型
- AtomicLong:原子更新长整型
- Striped64:累加器
- LongAccumulator:Striped64 实现类
- LongAdder:Striped64 实现类
- DoubleAccumulator:Striped64 实现类
- DoubleAdder:Striped64 实现类
-
数组
- AtomicIntegerArray:原子更新整型数组里的元素
- AtomicLongArray:原子更新长整型数组里的元素
- AtomicReferenceArray:原子更新引用类型数组里的元素
-
引用类型
- AtomicReference:原子更新引用类型
- AtomicMarkableReference:原子更新带有标记位的引用类型
- AtomicStampedReference:原子更新带有版本号的引用类型
-
字段类(即对象的属性修改类型)
- AtomicIntegerFieldUpdater:原子更新整型字段的更新器
- AtomicLongFieldUpdater:原子更新长整型字段的更新器
- AtomicReferenceFieldUpdater:原子更新引用类型的字段
1. atomic 的原理
Atomic 包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以像自旋锁一样,不断重试,直到执行成功
Atomic 系列的类中的核心方法都会调用 unsafe 类中的几个本地方法。Unsafe 类全名为:sun.misc.Unsafe,这个类包含了大量 C 代码的操作,包括很多直接内存分配以及原子操作的调用,而它之所以标记为非安全的,是告诉我们这个类里面大量的方法的调用都会存在安全隐患,需要小心使用,否则会导致严重的后果,例如在通过 unsafe 分配内存的时候,如果自己指定某些区域可能会导致一些类似 C++ 一样的指针越界到其他进程的问题
2. 精讲
1. Striped64 类的实现
这个类是个抽象类,里面主要实现了两个方法 longAccumulate,doubleAccumulate 分别对 long 和 double 类型的数据进行一个累计。主要提供给 LongAdder 、LongAccumulator 、DoubleAdder 、DoubleAccumulator 进行一个调用。可以理解为用 cells 的方式来减少并发时产生的冲突。
源码:
Striped64 中的 Cell 类:
@sun.misc.Contended static final class Cell {
volatile long value;
Cell(long x) { value = x; }
final boolean cas(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
}
// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
private static final long valueOffset;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> ak = Cell.class;
valueOffset = UNSAFE.objectFieldOffset
(ak.getDeclaredField("value"));
} catch (Exception e) {
throw new Error(e);
}
}
}
Cell 类中仅有一个保存计数的变量 value,并且为该变量提供了 CAS 操作方法,Cell 类的实现虽然看起来很简单,但是它的作用是非常大的,它是 Striped64 实现分散计数的最为基础的数据结构,当然为了达到并发环境下的线程安全以及高效,Striped64 做了很多努力。Striped64 中有两个提供计数的 api 方法,分别为 longAccumulate和doubleAccumulate,两者的实现思路是一致的,只是前者对long类型计数,而后者对double类型计数。
longAccumulate 本身的逻辑并不复杂,只是因为使用无锁,使用了死循环,并包含了大量的检测代码,因为在理解过程中,一般不需要对具体的无锁技巧进行深度理解(如果水平到了也可以这样做),只需要结合其设计思想,理解设计思想即可。
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
int h;
if ((h = getProbe()) == 0) { //获取当前线程的probe值,如果为0,则需要初始化该线程的probe值
ThreadLocalRandom.current(); // force initialization
h = getProbe();
wasUncontended = true;
}
boolean collide = false; // True if last slot nonempty
for (;;) {
Cell[] as; Cell a; int n; long v;
if ((as = cells) != null && (n = as.length) > 0) { //获取cell数组
if ((a = as[(n - 1) & h]) == null) { // 通过(hashCode & (length - 1))这种算法来实现取模
if (cellsBusy == 0) { // 如果当前位置为null说明需要初始化
Cell r = new Cell(x); // Optimistically create
if (cellsBusy == 0 && casCellsBusy()) {
boolean created = false;
try { // Recheck under lock
Cell[] rs; int m, j;
if ((rs = cells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;
}
} finally {
cellsBusy = 0;
}
if (created)
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
//运行到此说明cell的对应位置上已经有想相应的Cell了,不需要初始化了
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
//尝试去修改a上的计数,a为Cell数组中index位置上的cell
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
//cell数组最大为cpu的数量,cells != as表面cells数组已经被更新了
else if (n >= NCPU || cells != as)
collide = false; // At max size or stale
else if (!collide)
collide = true;
else if (cellsBusy == 0 && casCellsBusy()) {
try {
if (cells == as) { // Expand table unless stale
Cell[] rs = new Cell[n << 1]; //Cell数组扩容,每次扩容为原来的两倍
for (int i = 0; i < n; ++i)
rs[i] = as[i];
cells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
h = advanceProbe(h);
}
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
boolean init = false;
try { // Initialize table
if (cells == as) {
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
cells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break; // Fall back on using base
}
}
- longAccumulate 会根据当前线程来计算一个哈希值,然后根据算法 (hashCode & (length - 1)) 来达到取模的效果以定位到该线程被分散到的 Cell 数组中的位置
- 如果 Cell 数组还没有被创建,那么就去获取 cellBusy 这个共享变量(相当于锁,但是更为轻量级),如果获取成功,则初始化 Cell 数组,初始容量为 2,初始化完成之后将 x 保证成一个 Cell,哈希计算之后分散到相应的 index 上。如果获取 cellBusy 失败,那么会试图将 x 累计到 base 上,更新失败会重新尝试直到成功。
- 如果 Cell 数组以及被初始化过了,那么就根据线程的哈希值分散到一个 Cell 数组元素上,获取这个位置上的 Cell 并且赋值给变量 a,这个 a 很重要,如果 a 为 null,说明该位置还没有被初始化,那么就初始化,当然在初始化之前需要竞争 cellBusy 变量。
- 如果 Cell 数组的大小已经最大了(CPU的数量),那么就需要重新计算哈希,来重新分散当前线程到另外一个 Cell 位置上再走一遍该方法的逻辑,否则就需要对 Cell 数组进行扩容,然后将原来的计数内容迁移过去。这里面需要注意的是,因为 Cell 里面保存的是计数值,所以在扩容之后没有必要做其他的处理,直接根据 index 将旧的 Cell 数组内容直接复制到新的 Cell 数组中就可以了。
当然,上面的流程是高度概括的,longAccumulate 的实际分支还要更多,并且为了保证线程安全做的判断更多。longAccumulate会根据不同的状态来执行不同的分支,比如在线程竞争非常激烈的时候,会通过对cells数组扩容或者从新计算哈希值来重新分散线程,这些做法的目的是将多个线程的计数请求分散到不同的 cells 的 index 上,其实这和 java7 中的 ConcurrentHashMap 的设计思路是完全一致的,但是 java7 中的 ConcurrentHashMap 实现在 segment 加锁使用了比较重的 synchronized,而 Striped64 使用了 java 中较为底层的 Unsafe 类的 CAS 操作来进行并发操作,这种方式更为轻量级,因为它会不停的尝试,失败会返回,而加锁的方式会阻塞线程,线程需要被唤醒,这涉及到了线程的状态的改变,需要上下文切换,所以是比较重量级的。