前言
本文所有代码请添加vm参数运行-XX:-RestrictContended
防止注解失效。
LongAccumulator
DoubleAccumulator
在多线程进程情况,我们可能希望计算某一共享变量,如页面访问次数,商品剩余数量秒杀。本文仅用LongAccumulator
做说明。正确的使用累加器可以提供并发效率,如下面的测试案例中两个都是多线程安全的无锁方式,但是效率相差14倍。
LongAccumulator
基础使用
public static void main(String[] args) throws InterruptedException {
LongAccumulator longAccumulator = new LongAccumulator(new LongBinaryOperator() {
@Override
public long applyAsLong(long left, long right) {
//当前值和最新需要累加值
//left指向当前longAccumulator存储的值,right是需要累加的值
//比如longAccumulator.accumulate(2); 那么right为2
return left + right;
}
}, 10);
//加2
longAccumulator.accumulate(2);
//输出12
System.out.println(longAccumulator.get());
//重置为初始化的数值为10
longAccumulator.reset();
//输出10
System.out.println(longAccumulator.get());
//累加当前值,运行后为7
longAccumulator.accumulate(-3);
//得到当前值然后重置为初始值10
System.out.println(longAccumulator.getThenReset());
}
其实上述代码可以用AtomicLong
实现,但是为什么JUC
却而外提供了这样一个类供我们使用。
我们构造30个线程,每个线程堆共享变量自增10000000
次。
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
LongAccumulator longAccumulator =
new LongAccumulator(((left, right) -> left + right), 0);
Runnable run = () -> {
int i = 10000000;
while (--i > 0) {
longAccumulator.accumulate(1);
}
};
//构造1000个线程
List<Thread> collect = IntStream
.range(0, 30)
.mapToObj((int value) -> new Thread(run))
.collect(Collectors.toList());
//启动他们
collect.forEach(Thread::start);
//等候他们完成
collect.forEach((thread -> {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}));
System.out.println("结果" + longAccumulator.get());
long end = System.currentTimeMillis();
System.out.println("耗时" + (end - start));
}
输出:
结果299999970
耗时439
我们在用AtomicLong
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
AtomicLong longAccumulator = new AtomicLong();
Runnable run = () -> {
int i = 10000000;
while (--i > 0) {
longAccumulator.incrementAndGet();
}
};
//构造1000个线程
List<Thread> collect = IntStream
.range(0, 30)
.mapToObj((int value) -> new Thread(run))
.collect(Collectors.toList());
//启动他们
collect.forEach(Thread::start);
//等候他们完成
collect.forEach((thread -> {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}));
System.out.println("结果" + longAccumulator.get());
long end = System.currentTimeMillis();
System.out.println("耗时" + (end - start));
}
输出:
结果299999970
耗时6240
为了正确理解效率相差之大我们必须学习原理
CPU缓存原理
上图中每个Core1 Core2 ...
表示每个cpu
核心。
由于cpu
计算速度远远大于内存读写速度,所以我们往往会在内存和缓存之间加入若干级别的缓存硬件,越靠近cpu的我们编号越低 速度也越快,比如L1
缓存读写速度大于L2
,而每个CPU核心都有自己独立的缓存器,这也带来了可见性相关问题。(可见性问题这里不在过多说明)
L1
缓存器和内存最小的交换的单位是缓存行,即如果内存想要缓存变量A
,会同时缓存内存的其他变量直到缓存行的大小。
上图缓存器中存在多个缓存行缓存内存变量流程:
缓存器会一次性缓存内存中一个缓存行大小的。
Cpu修改缓存变量
假设Cpu
修改L1缓存
中的变量A
,那么变量A
所在缓存行的所有变量会全部更新到内存中,而不是单纯变量A
Cpu缓存更新带来的问题
缓存虽然提高了整体的计算能力,但是也带来部分弊端。
假设我们有多个Cpu1,Cpu2...
需要同时对共享变量A
进行读写操作,而对共享变量B
仅进行读取操作,那么很有可能变量A,变量B
被放入同一个缓存行中,由于变量A
需要时常同步到内存和缓存中,导致读取变量B
时也要等候缓存完成才能读取。
如下图所示:
那么我们有什么办法可以解决呢?空间换时间
方式可以让我们避免变量A
和变量B
被放入一个同一个缓存行。我们在变量A临近的附近插入无关变量直到填充整个缓存行大小。
案例代码:
案例来自参考文章修改
class PersonValue {
volatile public long value;
}
class Person extends Thread {
public final static long ITERATIONS = 500L * 1000L * 1000L;
static PersonValue[] personValuesArr = new PersonValue[2];
static {
personValuesArr[0] = new PersonValue();
personValuesArr[1] = new PersonValue();
}
int index = 0;
public Person(int index) {
this.index = index;
}
@Override
public void run() {
long i = ITERATIONS;
while (--i > 0) {
personValuesArr[index].value = i;
}
}
}
public class TestContend {
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
Person person = new Person(0);
Person person1 = new Person(1);
person.start();
person1.start();
person.join();
person1.join();
long end = System.currentTimeMillis();
System.out.println("耗时" + TimeUnit.MILLISECONDS.toSeconds(end - start));
}
}
我们看下输出的耗时:耗时22
我们简单修改PersonValue
类
class PersonValue {
//填充缓存行
volatile long p0, p1, p2, p3, p4, p5, p6;
volatile public long value;
//填充缓存行
volatile long q0, q1, q2, q3, q4, q5, q6;
}
输出结果:耗时3
为了方便我们这样做JDK提供了注解,下面注解可以省去我们手动写填充代码。
@Contended
class PersonValue {
volatile public long value;
}
运行这个注解请添加vm参数-XX:-RestrictContended
另一种形式的代码也是同理:
@Contended
class PersonValue {
volatile public long value;
}
class Person extends Thread {
public final static long ITERATIONS = 500L * 1000L * 1000L*4;
// static PersonValue[] personValuesArr = new PersonValue[2];
public static PersonValue personValuesA = new PersonValue();
public static PersonValue personValuesB = new PersonValue();
// static {
// personValuesArr[0] = new PersonValue();
// personValuesArr[1] = new PersonValue();
// }
int index = 0;
public Person(int index) {
this.index = index;
}
@Override
public void run() {
long i = ITERATIONS;
while (--i > 0) {
if (index == 0) {
personValuesA.value = i;
} else {
personValuesB.value = i;
}
// personValuesArr[index].value = i;
}
}
}
但需要注意以下代码是不符合填充的:
静态共享变量不能使用上述伪代码
class Person extends Thread {
public final static long ITERATIONS = 500L * 1000L * 1000L*20;
public static int personValuesA = 3;
@Contended
public static int personValuesB = 1;
int index = 0;
public Person(int index) {
this.index = index;
}
@Override
public void run() {
long i = ITERATIONS;
while (--i > 0) {
if (index == 0) {
personValuesA = 3;
} else {
personValuesB = 3;
}
}
}
}
输出:
耗时3
我们去掉标记后
class Person extends Thread {
//...
public static int personValuesA = 3;
public static int personValuesB = 1;
//...
}
输出:
耗时3
你可以将内部属性抽离成一个类:
class PersonValue {
volatile public int valueA;
@Contended
volatile public int valueB;
}
class Person extends Thread {
public final static long ITERATIONS = 500L * 1000L * 1000L ;
public static volatile PersonValue personValue = new PersonValue();
int index = 0;
public Person(int index) {
this.index = index;
}
@Override
public void run() {
long i = ITERATIONS;
while (--i > 0) {
if (index == 0) {
personValue.valueA = 3;
} else {
personValue.valueB = 3;
}
}
}
}
关于为什么静态字段不能使用上述代码避免伪共享原因,笔者也没有清晰的答案。只能猜测java
静态属性只有读取才会载入内存,且只会加载被读取属性而不是整个类。当然这是我的个人意淫想法,我提问在Stack Overflow
了坐等大神解决。
Java false sharing in multiple thread problem 笔者意淫图:
源码分析
理解原理后笔者看了一遍源码,笔者居然在concurrentHashmap分析过,这里就不写了。