在JVM中long和double型变量都是占用8个字节空间存储的, 而在读写时,是以4字节为单位操作的; 也就是要写入一个long型数据, 需要分别写入高位和低位, 共2次完成.
所以long和double是天生的线程不安全; 要在线程间共享long或者double变量, 必须放在锁内或synchronized代码块中执行, 或是将变量声明为volatile类型.
AtomicLong
在并发较少场景下, 可以使用AtomicLong解决并发的原子性问题, 与之前讲的AtomicInteger类似,利用volatile和CAS机制完成. 因为每次读写都是2次操作,相对AtomicInteger而言效率是较低的.
LongAdder
JDK8中, 为优化AtomicLong在高并发下的低效问题, 引入了一个新的Long型原子操作类LongAdder, 它比AtomicLong拥有更好的性能, 当然代价就是消耗更多的空间.
数据写入
在LongAdder中数据是其父类Striped64中的两个变量base, cells[]共同存储的.
代码语言:javascript复制/**
* Table of cells. When non-null, size is a power of 2.
*/
transient volatile Cell[] cells;
/**
* Base value, used mainly when there is no contention, but also as
* a fallback during table initialization races. Updated via CAS.
*/
transient volatile long base;
Striped64: 是一个处理累加的高并发工具类.
base: 是在没有线程竞争时, 数据的CAS处理部分;
cells[]: 是在有线程竞争时, 数据的处理部分;
有无线程竞争的判断依据就是在对base进行CAS操作时是否成功.
代码语言:javascript复制casBase(b = base, b x)
在使用cells, 首先会根据当前线程ID和数组长度, 计算该使用cells数组中哪个元素进行CAS计算.
代码语言:javascript复制(a = as[getProbe() & m]) == null
综上, 采用了分治的思想, 数据操作由原来1个位置, 分散到了base和cells[]数组的多个位置, 降低了数据锁的概率, 提高了运算效率.
数据读取
数据的读取是base和cells[]数组累加得到的.
在高并发下, 累计过的数据可能被其他线程修改了, 导致出现误差.
Cells[]初始化
执行写入出现竞争时, cells[]数组就会调用longAccumulate()方法进行初始化处理
代码语言:javascript复制public void add(long x) {
...
!(uncontended = a.cas(v = a.value, v x)))
longAccumulate(x, null, uncontended);
...
}
Cells[]初始化时会根据线程的探针哈希值初始化cells[]数组长度.
探针哈希值的作用是将线程和数组中的不同元素对应起来, 尽量避免线程争用同一数组元素.
代码语言:javascript复制static final int getProbe() {
return UNSAFE.getInt(Thread.currentThread(), PROBE);
}
LongAdder与AtomicLong的比较
LongAdder和AtomicLong虽都能实现对long型数字的计数, 但他们还是有些区别的.
1. 从API上说, LongAdder只提供了加1和减1的相关方法, 而AtomicLong可以处理任意数, 使用上也更灵活, 也提供了更多的使用方式.
2. 从性能上说, 在并发量高的情况下, LongAdder造成锁的概率更低, 性能更高, 但在get时可能有误差. 一般情况下, AtomicLong都是可以满足性能需要的.