一、原子累加器
我们都知道,原子整型可以在线程安全的前提下做到累加功能,而今天介绍的LongAdder具有更好的性能
我们先来看原子累加器和原子整型做累加的对比使用:
private static <T> void demo(Supplier<T> supplier, Consumer<T> action){
T adder = supplier.get();
long start = System.nanoTime();
List<Thread> ts = new ArrayList<>();
for (int i = 0;i<40;i++){
ts.add(new Thread(()->{
for (int j = 0;j<500000;j++){
action.accept(adder);
}
}));
}
ts.forEach(t->t.start());
ts.forEach(t->{
try {
t.join();
}catch (InterruptedException e){
e.printStackTrace();
}
});
}
public static void main(String[] args) {
for (int i = 0;i<5;i++){
demo(()->new LongAdder(),longAdder -> longAdder.increment());
}
for (int i = 0;i<5;i++){
demo(()->new AtomicLong(),atomicLong -> atomicLong.getAndIncrement());
}
}
通过上面的代码运行我们可以清晰地看到,两种方式都有效的产完成了累加的效果,但是明显使用累加器的效率要更好,甚至要高出原子类型累加好几倍。
现在,我们可以简单地理解为,原子累加器就是JAVA在并发编程下提供的有保障的累加手段。
二、LongAdder执行原理
LongAdder之所以性能提升这么多,就是在有竞争时,设置多个累加单元,不同线程累加不同的累加单元,最后在将其汇总,减少了cas重试失败,从而提高了性能。
简单来说,累加器就是将需要做累加的共享变量,分成许多部分,时多个线程只累加自己的部分(这样做既可以减少使用普通整型容易出现的线程不安全错误,也可以提高原子类型在累加时效率底下的问题),足以后再将结果汇总,得到累加结果。
就比如银行要清点大量钞票,一个人来清点效率低下,所以需要多个人来(多个线程),将大量钞票分给多个员工(分配累加单元),每个员工仅仅需要对自己的那部分钞票清点(每个线程累加自己的累加单元),最后将结果汇总起来,就是所有钞票的总数。
LongAdder中有几个关键域:
transient volatile Cell[] cells;//累加单元数组,懒惰初始化
transient volatile long base;//基础值,如果是单线程没有竞争,则用cas累加这个域
transient volatile int cellsBusy;//zaicell创建或扩容时,置位1,表示加锁
其中cells就是累加单元,用来给各个线程分配累加任务。
base用来做单线程的累加,同时还有汇总的作用,也是就是说base=cells[0]+cells[1]……+base
cellbase则用来表示加锁置位,0表示无锁,1表示加锁。
注意:
此处cellsbusy所说的锁并不是真正的对象锁,而是底层用cas来模拟加锁。
cas模拟加锁:
当占用某资源需要模拟加锁时,cas会将某处标志位的初始态变为加锁态(如将0改为1)。此时当出现竞争时,其他线程同样会尝试cas操作,但均以失败告终,所以会不停尝试cas,起到了加锁的功效。
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类源码中我们能看出,cell底层也是采用cas来做累加计数。
三、伪共享
我们都知道cpu内存模型
因为 CPU 与内存的速度差异很大,需要靠预读数据至缓存来提升效率。而缓存以缓存行为单位,每个缓存行对应着一块内存,一般是64 byte (8个 long )
缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中。
CPU 要保证数据的一致性,如果某个 CPU 核心更改了数据,其它 CPU 核心对应的整个缓存行必须失效
也就是说,在同一缓存行中的缓存的内容时时刻刻都是相同的,这样就违背了我们cells的初衷。
如图,因为cells是数组类型,导致他们在内存中始终处于连续存储状态。当任何一个线程改变cell中的值时,另一个线程中的缓存必然会失效(缓存是以行为单位进行更新),这样繁杂的操作使得效率大大降低。
问题解决
@ sun . misc . Contended 用来解决这个问题,它的原理是在使用此注解的对象或字段的前后各增加128字节大小的 padding 从而让 CPU 将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效。
也就是说,增加无用的内存空间是cells扩容,从而在缓存中,每一个cell能占据一个缓存行,也就解决了失效的问题。