【多线程:cas】原子更新器 原子累加器 缓存一致性问题
01.原子更新器
介绍
原子更新器又叫字段更新器,作用是成员变量更新时保证原子性
AtomicReferenceFieldUp:成员变量为引用类型时
AtomicIntegerFiledUpdater:成员变量是整型
AtomicLongFiledUpdater:成员变量是长整型
这里拿AtomicReferenceFieldUp举例
AtomicReferenceFieldUp
@Slf4j(topic = "c.Test40")
public class Test40 {
public static void main(String[] args) {
Student stu = new Student();
AtomicReferenceFieldUpdater updater =
AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name");
System.out.println(updater.compareAndSet(stu, null, "张三"));
System.out.println(stu);
}
}
class Student {
volatile String name;
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
'}';
}
}
结果
true
Student{name='张三'}
解释
可以看出cas操作成功,并且打印结果也说明更新成功,不过要注意一点,因为cas操作要求必须是可见的 所以成员变量必须用volatile修饰
02.原子累加器:LongAdder
介绍
累加器顾名思义就是累加的,不过可能有同学回问之前不是已经有getAndIncrement()这个方法了吗?为什么还需要专门的累加器,原因很简单就是这个原子累加器效率更高。
代码
public class Test41 {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
demo(
() -> new AtomicLong(0),
(adder) -> adder.getAndIncrement()
);
}
System.out.println("s");
for (int i = 0; i < 5; i++) {
demo(
() -> new LongAdder(),
adder -> adder.increment()
);
}
}
/*
() -> 结果 提供累加器对象
(参数) -> 执行累加操作
*/
private static <T> void demo(Supplier<T> adderSupplier, Consumer<T> action) {
T adder = adderSupplier.get();
List<Thread> ts = new ArrayList<>();
// 4 个线程,每人累加 50 万
for (int i = 0; i < 4; i++) {
ts.add(new Thread(() -> {
for (int j = 0; j < 500000; j++) {
action.accept(adder);
}
}));
}
long start = System.nanoTime();
ts.forEach(t -> t.start());
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.nanoTime();
System.out.println(adder + " cost:" + (end - start) / 1000_000);
}
}
结果
2000000 cost:42
2000000 cost:27
2000000 cost:34
2000000 cost:27
2000000 cost:35
s
2000000 cost:16
2000000 cost:13
2000000 cost:6
2000000 cost:6
2000000 cost:6
解释
可以明显看出原子累加器效率要高很多。性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加 Cell[1]... 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能。
03.LongAdder源码
LongAdder类有几个关键域
// 累加单元数组,懒惰初始化
transient volatile Cell[] cells;
// 基础值,如果没有竞争,则用cas累加这个域
transient volatile long base;
// 在cells创建或者扩容时 置为1 表示加锁
transient volatile int cellBusy;
cas锁:cellBusy实现
@Slf4j(topic = "c.Test42")
public class LockCas {
// 0 没加锁
// 1 加锁
private AtomicInteger state = new AtomicInteger(0);
public void lock() {
while (true) {
if (state.compareAndSet(0, 1)) {
break;
}
}
}
public void unlock() {
log.debug("unlock...");
state.set(0);
}
public static void main(String[] args) {
LockCas lock = new LockCas();
new Thread(() -> {
log.debug("begin...");
lock.lock();
try {
log.debug("lock...");
sleep(1);
} finally {
lock.unlock();
}
}).start();
new Thread(() -> {
log.debug("begin...");
lock.lock();
try {
log.debug("lock...");
} finally {
lock.unlock();
}
}).start();
}
}
解释
state就相当于我们的cellBusy,加锁时用cas把0变为1 之后其他线程再想执行这段代码发现cas比较值不同 cas失败相当于获取锁失败,之后解锁时此线程调用set方法把state重新置为0 实现解锁
缓存一致性:Cell类源码
Cell类的源码很容易理解,不过它的注解引起了我们的 @sun.misc.Contended,这个注解是为了防止缓存行伪共享 也就是缓存一致性
缓存一致性
我们之前了解过java内存模型(JMM)和上面的图十分的相似 把cpu换位线程 把缓存换位工作内存就基本一样了,其实JMM并不是真实存在的它是jvm层面上对操作系统内存模型的模拟,上面的图片就是我们电脑里的真实内存形式。
cpu可以直接从内存中获取数据,但是速度相对缓存而言慢了好几倍,所以通常我们的处理形式就是把内存中的数据拷贝到缓存中去,然后cpu从缓存中获取数据,但是这样就会出现一个问题 那就是缓存一致性。
什么是缓存一致性
我们来看这张图,假如我们现在有一个Cell数组,cell[0] cell[1],我们知道数组是在内存中是挨着的,而我们的缓存以缓存行的形式存放这 一个缓存行对应一块内存(64byte) 而我们现在的这个Cell数组 占48byte(一个cell占24byte),我们把数组从内存中拷贝到缓存中时 并不是只拷贝你需要的那个数据 而是拷贝一整个缓存行的数据 避免了多次拷贝数据导致性能降低,也就是cpu1会拷贝这个数组 cpu2也会拷贝这个数组,cpu1负责改变cell[0]的数据,cpu2赋值改变cell[1]的数据,但是最终同步数据到内存只会有一个成功 也就是势必有一个cpu的缓存中的数组失效,导致只改变了一个数据 假如只改变了cell[0]的数据 那么cpu肯定就需要再次拷贝这整个数组修改cell[1],这样相当于一个数组我们修改了两次才成功 如果数组元素多了呢 性能就会大大降低,这个就是缓存一致性问题。
如何解决缓存一致性问题
解决方法很简单就是上述提到的 @sun.misc.Contended 这个注解,这个注解的作用是在此注解对象或字段的前后各增加128byte 也就是前后各占一个缓存行的大小,这样的好处是 保证数据绝对不在一个缓存行内,使得 cpu读取的数据是单独的 避免对方缓存行失效的问题。解决缓存一致性的方法是典型的以空间换时间处理方式。