JUC包提供了一系列常用数据结构的原子类,这些类位于Java.util.concurrent.atomic
包下,这些类都是使用CAS实现的,相比使用锁的方式在性能上有了很大的提升,可以再高并发的场景下,保证线程安全的同时,以更简单、高效的方式操作一个共享变量。
一、AtomicLong
类似类:AtomicInteger
、AtomicIntegerArray
、AtomicBoolean
、AtomicLongArray
、AtomicReference
、AtomicReferenceArray
原子变量操作类主要使用Unsafe类的CAS原子更新方法实现原子递增或递减操作。
主要属性:
// setup to use Unsafe.compareAndSwapLong for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
/**
* Records whether the underlying JVM supports lockless
* compareAndSwap for longs. While the Unsafe.compareAndSwapLong
* method works in either case, some constructions should be
* handled at Java level to avoid locking user-visible locks.
*/
static final boolean VM_SUPPORTS_LONG_CAS = VMSupportsCS8();
/**
* Returns whether underlying JVM supports lockless CompareAndSet
* for longs. Called only once and cached in VM_SUPPORTS_LONG_CAS.
*/
private static native boolean VMSupportsCS8();
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicLong.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile long value;
二、LongAddr
在JDK1.8中新增了四个高性能原子类,这四个类使用了分段锁的思想,将不同的线程映射到不同的数据段上去更新,将这些段的值相加就能得到最终的值。
LongAccumulator
:Long类型的聚合器,需要传入一个Long类型的二元操作,可以用来计算各种集合操作,包括加乘;DoubleAccumulator
:Double类型聚合器,需要传入一个DOuble类型的二元操作,可以用来计算各种聚合操作,包括加乘等;LongAdder
:Long类型累加器,是LongAccumulator
的特例,只能用来计算加法,从0开始计算;DoubleAdder
:Double类型累加器,是DoubleAccumulator
的特例,只能用来计算加法,从0开始计算;
LongAddr
继承自Striped64
,Striped64
是一个抽象类,用来实现累加功能,它是实现高并发累加的工具类。在Striped64
定义了三个变量,LongAddr
真实值其实就是将base的值和cells数组元素值累加。
AtomicLong
使用内部变量value保存着实际的long值,所有线程的读写操作都是针对一个变量进行的,也就说在高并发环境下,N个线程会同时竞争一个热点value变量的读写。
而LongAddr
的基本思路就是分散并发冲突的热点。在没有线程竞争的情况下,将要累加的数累计额到一个热点base基础值上;当有线程竞争的时候,将热点分散到cells数组中,不同线程映射到不同的数组位置上,每个线程对应位置中的元素进行CAS递增或者递减操作,这样就可以从一个热点分散成多个热点,发生并发冲突的概率就会小很多。
在LongAddr
中,与其说是LongAddr
,不如说是Striped64
中有三个重要的变量:
base
:在没有线程竞争的时候,执行递增、递减操作会直接CAS更新该值;cells
:为Cell
类型数组,在有线程竞争时,每个线程会映射到对应的cells[i]的位置上进行CAS操作;cellsBusy
:在cells初始化时、进行扩容时、槽进行初始化时使用,保证cells变量的线程安全。0:锁空闲;1:锁被占用
如果要获取LongAddr
当前值,需要将base和cells数组中所有元素累加起来。
其他属性:
// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
// base字段的内存偏移量
private static final long BASE;
// cellsBusy字段的内存偏移量
private static final long CELLSBUSY;
// Thread的threadLocalRandomProbe字段内存偏移量,可以看成是线程的hash值,在计算
// 线程映射cells引用位置时使用。
private static final long PROBE;
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;
// value偏移量
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);
}
}
}
在LongAddr
中最主要的方法就是add
和sum
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
// cells不等于null,即将值直接累加到cells中,它认为之前发生过冲突,此时产生并发冲突的概率
// 也比较高,直接将值累加到cells数组中
// casBase,对于base属性的CAS操作,如果操作成功则直接加到base中,失败则下一步
if ((as = cells) != null || !casBase(b = base, b + x)) {
// 不冲突,这个指的是cells中元素是否冲突
boolean uncontended = true;
// as == null || (m = as.length - 1) < 0:cells还未初始化
// (a = as[getProbe() & m]) == null:当前线程映射到cells中相应位置还未初始化
// !(uncontended = a.cas(v = a.value, v + x))):更新值到映射cell,成功直接退出
if (as == null || (m = as.length - 1) < 0 ||
(a = as[getProbe() & m]) == null ||
!(uncontended = a.cas(v = a.value, v + x)))
longAccumulate(x, null, uncontended);
}
}
// toString默认调用该方法
public long sum() {
Cell[] as = cells; Cell a;
long sum = base;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
// 刷新实例为0
public void reset() {
Cell[] as = cells; Cell a;
base = 0L;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
a.value = 0L;
}
}
}
sum
在并发场景中,只能得到一个近似值,如果想得到绝对准确的值还是需要全局锁的。这也是LongAddr
不能完全代替LongAtomic
的原因之一
在Striped64
中最主要的方法:longAccumulate
/**
x:累加的值
fn:操作函数
wasUncontended:是否冲突过
**/
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
int h;
// h为0,则重新获取
if ((h = getProbe()) == 0) {
ThreadLocalRandom.current(); // force initialization
h = getProbe();
wasUncontended = true;
}
// 冲突后标记为true,并且rehash
boolean collide = false; // True if last slot nonempty
// 自旋循环,CAS累加值直至成功
for (;;) {
Cell[] as; Cell a; int n; long v;
// cells已经初始化完成
if ((as = cells) != null && (n = as.length) > 0) {
// 当前线程映射的位置还未初始化
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // Try to attach new Cell
Cell r = new Cell(x); // Optimistically create
// 初始化cell时,加锁
if (cellsBusy == 0 && casCellsBusy()) {
boolean created = false;
try { // Recheck under lock
Cell[] rs; int m, j;
// 初始化cell
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;
}
// Continue after rehash
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
// 将值累加到cells中,成功退出
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
// 当数组大于NCPU(Runtime.getRuntime().availableProcessors())CPU内核数
// cells过时
else if (n >= NCPU || cells != as)
collide = false; // At max size or stale
// 标记冲突,并且rehash后自旋重试
else if (!collide)
collide = true; // 下次自旋在下面分支进行扩容,不过在扩容前会再次尝试一下CAS
// rehash后依旧无法更新,则获取锁进行扩容
else if (cellsBusy == 0 && casCellsBusy()) {
try {
if (cells == as) { // Expand table unless stale
// 动态扩大cells至两倍
Cell[] rs = new Cell[n << 1];
// 将原来cells中的值,移到相同的位置
for (int i = 0; i < n; ++i)
rs[i] = as[i];
cells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
// 刷新当前线程的hash
h = advanceProbe(h);
}
// cells还未初始化且加锁成功
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
// 是否初始化成功
// 只有一种可能失败,那就是加锁后,cells已经被其他线程抢先初始化
boolean init = false;
try { // Initialize table
// 判断是否被其他线程初始化
if (cells == as) {
// cells中数量一定是2的幂数
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
cells = rs;
init = true;
}
} finally {
// 释放锁
cellsBusy = 0;
}
if (init)
break;
}
// cells还未初始化并且加锁未成功,这表示其他线程正在初始化,尝试更新base
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break; // Fall back on using base
}
}
cellsBusy
在三种情况下进行加锁:
- cells数组初始化
- cells数组扩容迁移
- cells数组中某个槽初始化
注:本文为《Java修炼指南:高频源码解析》阅读笔记