之前遇到一个问题,写一个线程安全的高效计数。题目就这一句话,通过对这个问题的思考展开本篇。
一 、初步想法
-
synchronized控制变量的修改
加锁的方式会阻塞线程,线程需要被唤醒,这涉及到了线程的状态的改变,需要上下文切换,所以是比较重量级的,-- 可以用但是低效。
-
volatile修饰计数变量
volatile只能保证多线程的内存可见性,不能保证多线程的执行有序性。而最基本的同步要保证有序性和可见性。-- 完全不可用。
-
Atomic* 系列的变量
涉及并发的地方都是使用CAS操作,使用
sun.misc.Unsafe
提供的一系列底层 API,使得 Java 这样的高级语言能够直接和硬件层面的 CPU 指令打交道,在硬件层次上去做 compare and set操作。效率非常高。 -
Java8以后提供的LongAdder(继承Striped64抽象类)
和Atomic* 系列一样, 涉及并发的地方都是使用CAS操作,使用
sun.misc.Unsafe
提供的一系列底层 API,但是在多线程的情况下,它的效率更好,下面会从源码分析一下。
我们主要比较一下后面两种
二 、写测试
package com.su.demo;
import org.springframework.util.StopWatch;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
/**
* Author: susq
* Date: 2019-08-17 11:07
*/
public class CountApp {
// 分别用10, 100, 1000
private static ExecutorService executorService = Executors.newFixedThreadPool(100);
public static void main(String[] args) {
StopWatch stopWatch = new StopWatch("100个线程几乎同时计数,每个线程计数100W次, 使用 LongAdder\n");
stopWatch.start("使用 LongAdder");
System.out.println(longAddrTest());
stopWatch.stop();
System.out.println(stopWatch.prettyPrint());
while (!executorService.isShutdown()) {
executorService.shutdown();
}
}
private static long atomicAddTest() {
AtomicLong num = new AtomicLong(0);
List<CompletableFuture> completableFutureList = new ArrayList<>();
CompletableFuture[] completableFutures = new CompletableFuture[]{};
for (int i = 0; i < 100; i++) {
completableFutureList.add(CompletableFuture.supplyAsync(() -> {
for (int j = 0; j < 1000000; j++) {
num.incrementAndGet();
}
return new Object();
}, executorService));
}
CompletableFuture.allOf(completableFutureList.toArray(completableFutures)).join();
return num.get();
}
private static long longAddrTest() {
LongAdder num = new LongAdder();
List<CompletableFuture> completableFutureList = new ArrayList<>();
CompletableFuture[] completableFutures = new CompletableFuture[]{};
for (int i = 0; i < 100; i++) {
completableFutureList.add(CompletableFuture.supplyAsync(() -> {
for (int j = 0; j < 1000000; j++) {
num.increment();
}
return new Object();
}, executorService));
}
CompletableFuture.allOf(completableFutureList.toArray(completableFutures)).join();
return num.longValue();
}
}
三 、 测试结果
1. 单线程执行
AtomicLong执行
1000000
StopWatch '1个线程几乎同时计数,每个线程计数100W次, 使用 AtomicLong
': running time (millis) = 47
-----------------------------------------
ms % Task name
-----------------------------------------
00047 100% 使用 AtomicLong
LongAdder执行
1000000
StopWatch '1个线程几乎同时计数,每个线程计数100W次, 使用 LongAdder
': running time (millis) = 51
-----------------------------------------
ms % Task name
-----------------------------------------
00051 100% 使用 LongAdder
2. 10个线程
AtomicLong执行
10000000
StopWatch '10个线程几乎同时计数,每个线程计数100W次, 使用 AtomicLong
': running time (millis) = 251
-----------------------------------------
ms % Task name
-----------------------------------------
00251 100% 使用 AtomicLong
LongAdder执行
10000000
StopWatch '10个线程几乎同时计数,每个线程计数100W次, 使用 LongAdder
': running time (millis) = 68
-----------------------------------------
ms % Task name
-----------------------------------------
00068 100% 使用 LongAdder
3. 100个线程
AtomicLong执行
100000000
StopWatch '100个线程几乎同时计数,每个线程计数100W次, 使用AtomicLong
': running time (millis) = 2063
-----------------------------------------
ms % Task name
-----------------------------------------
02063 100% 使用AtomicLong
LongAdder执行
StopWatch '100个线程几乎同时计数,每个线程计数100W次, 使用 LongAdder
': running time (millis) = 194
-----------------------------------------
ms % Task name
-----------------------------------------
00194 100% 使用 LongAdder
4. 1000个线程执行
AtomicLong执行
1000000000
StopWatch '1000个线程几乎同时计数,每个线程计数100W次, 使用 AtomicLong
': running time (millis) = 19883
-----------------------------------------
ms % Task name
-----------------------------------------
19883 100% 使用 AtomicLong
LongAdder执行
StopWatch '1000个线程几乎同时计数,每个线程计数100W次, 使用 LongAdder
': running time (millis) = 1065
-----------------------------------------
ms % Task name
-----------------------------------------
01065 100% 使用 LongAdder
结论:
看的出来,随着并发线程数的增加,两者的效率差距主键拉大。(这里针对的是写的效率,读的效率后面说)
四、为什么
-
LongAdder 继承 Striped64 继承 Number, 它在做运算的时候,先尝试cas更新,如果成功则与AtomicLong流程相似,如果失败,就不同了。它不会死循环不断地进项cas直到成功,而是将线程分散到不同的区域,减轻线程数量太多造成的大量失败,相当于分散唯一的计数值得热度。这个区域就是Cell 数组
-
Cell是Striped64 的内部类,包装了long value, 内部使用cas操作。增加了注解@sun.misc.Contended避免伪共享。缓存系统中是以缓存行(cache line)为单位存储的,当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。java8的这个注解避免伪共享的原理是在value前后各增加128字节大小的padding,使用2倍于大多数硬件缓存行的大小来避免相邻扇区预取导致的伪共享冲突。
@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);
}
}
}
- 核心方法就是add 方法 和 longAccumulate方法, 先使用cas, 如果失败,看看能不能分散到Cell 数组上去执行cas, 如果数组还没初始化,或者初始化了但是在定位后的位置cas操作失败了,则进入longAccumulate方法。
public class LongAdder extends Striped64 implements Serializable {
public void increment() {
add(1L);
}
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
/* 第一次调用的时候cells数组肯定为null
* final boolean casBase(long cmp, long val) {
* return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
* }
* 后面使用cas操作直接修改值,如果成功的时候,直接返回,就不用那
* 么复杂了,但是多线程的时候,cas操作是经常会失败的,线程越多失败越频繁,
* 这也是AtomicLong为什么在高并发时候效率降低的原因
*/
if ((as = cells) != null || !casBase(b = base, b + x)) {
boolean uncontended = true;
/* 如果 cells 没初始化,或者初始化了但是在cells数组的经过哈希得到的位置
* 处还没有值,或者在哈希后的位置处有值,但是对这个位置进项cas更新成功过了,就返回了
*/
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);
}
}
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
int h;
/* Probe 线程类中threadLocalRandomProbe属性的偏移量, 静态方法中初始化好了,
* getProbe()就是取线程中threadLocalRandomProbe属性的值,只要线程不同,这个值就不同,
* 可以很好的作为哈希值
*/
if ((h = getProbe()) == 0) {
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;
// 如果cells 数组已经初始化
if ((as = cells) != null && (n = as.length) > 0) {
// 如果 线程哈希值 和 数组长度取余(使用&操作取余效率更快)后的位置还没初始化值
if ((a = as[(n - 1) & h]) == null) {
// 判断锁标志cellsBusy,如果没有锁(==0表示没有锁),就创建一个新的值用Cell包装,
if (cellsBusy == 0) {
Cell r = new Cell(x);
// 在此检查没有锁,并且开始cas加锁,加锁成功后,将创建好的cell保存到数组中
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;
}
// 如果刚才保存成功了,created标志就是true, 可以结束了,
// 否则continue 重来整个循环里的操作
if (created)
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
/* 如果哈希冲突标志wasUncontended已经为true, 说明已经没有哈希冲突了。
* 如果为false,说明getProbe()返回的哈希值已经存在了但不是通过上面的强制
* 初始化线程得到的,通过该哈希值也找到数组对应的位置已经有值,但可能有冲突,
* 直接走最下面的方法h = advanceProbe(h); 再哈希更新h的值之后继续循环
*/
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
// 这里是哈希值是我们自己算过的了,定位到的元素也存在了,则尝试cas,成功了就退出
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
// 判断数组的长度已经达到了cpu核心数,collide 置为false,只要达到了,
// 每次循环执行到下一个else if就会短路扩容操作,免得继续增大数组
else if (n >= NCPU || cells != as)
collide = false; // At max size or stale
// 判断数组的长度已经没有达到了cpu核心数,collide 置为 true
else if (!collide)
collide = true;
// 如果cells数组容量还没有达到限制,并且分散后到指定cell上的cas也没成功,
// 则扩容,左移增大一倍。continue 继续下次循环
else if (cellsBusy == 0 && casCellsBusy()) {
try {
if (cells == as) { // Expand table unless stale
Cell[] rs = new Cell[n << 1];
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);
}
// 如果cells没有初始化,则尝试获取锁cellBusy,获取成功后初始化cells数
// 组大小为2,保存当前线程的值
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;
}
// 如果cells数组也初始化失败了,就尝试cas将结果保存到base字段上,
// 保存成功就结束,不成功继续下次循环
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break; // Fall back on using base
}
}
}
- 再来看读取方法,上面保存的时候,我们既将有些线程的值分担到各个cell去保存,但是也有部分累计到了base上面。可以看到获取的时候,是把base和所有cell里面的值一起累计起来返回的。所以读取的时候,虽然没有各类加锁的操作,但是却需要累加,是要比AtomicLong 慢一点点的。
public long longValue() {
return sum();
}
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;
}
五、各自的用处
- 很显然,对于多线程环境下频繁的更新计数操作,LongAdder 是最佳选择。但是读性能由于组合求值的原因,不如AtomicLong
- AtomicLong 提供的计数方法可以直接返回计算后的值,免去了再次读取的操作,对有些场景来说更方便。