Java并发编程7——Atomic家族2

  • 原子累加器(JDK1.8):AtomicLong和AtomicDouble的升级类型,专门用于数据统计,性能更高。
    • DoubleAccumulator
    • DoubleAdder
    • LongAccumulator
    • LongAdder
  • 伪共享问题
    • 缓存行失效 / 伪共享
    • @sun.misc.Contended注解
  • Unsafe对象

7.1 原子累加器

(1)LongAdder,DoubleAdder

  • 作用:虽然AtomicLong等原子类能够通过CAS进行原子累加操作,但在多线程竞争激烈的情况下很容易CAS失败,导致性能不高。原子累加器就是改进方法
  • 原理:底层有一个Base变量和Cell[]数组变量,加的时候首先尝试用CAS对base进行加,如果失败,那么转而对cell进行累加。这样同时争取一个变量的线程就变少了,而是分散成对多个变量的竞争,减少了失败次数。如果竞争某个Cell变量失败,它不会一直在这个Cell变量上自旋CAS重试,而是尝试在其他的Cell变量上进行CAS尝试,这个改变增加了当前线程重试CAS成功的可能性。最后,在获取LongAdder当前值时,是把所有Cell变量的value值累加后再加上base返回的

(2)LongAccumulator,DoubleAccumulator

  • 作用:LongAccumulator相比于LongAdder,可以为累加器提供非0的初始值,后者只能提供默认的0值。另外,前者还可以指定累加规则,比如不进行累加而进行相乘,只需要在构造LongAccumulator时传入自定义的双目运算器即可,后者则内置累加的规则

7.2 LongAdder / DoubleAdder

这里以LongAdder为例

(1)流程图

  • 由于cells数组懒加载,先判断cells数据是否为null——>如果为null说明线程竞争不激烈,CAS对base累加,累加失败再创建cells;如果不为null则对cells数组进行操作

(2)类图

  • Cell类是Striped64类中的静态内部类,LongAdder继承了Striped

在这里插入图片描述

(3)源码

-------------------------------Striped64类-------------------------------
//累加单元数组,惰性初始化
transient volatile Cell[] cells;
//如果没有竞争,则用cas累加这个base
transient volatile long base;
//在cells创建或扩容时,置为0,表示加锁;初始为0
transient volatile int cellsBusy;

//Striped64的静态内部类Cell
@sun.misc.Contended //防止缓存行伪共享
static final class Cell {
    volatile long value;
    Cell(long x) { value = x; }
    //最重要的方法,用CAS进行累加,pre表示旧值,next表示新值
    final boolean cas(long cmp, long val) {
        return UNSAFE.compareAndSwapLong(this, valueOffset, pre, next);
    }
}

(4)API

  • ----------------------------------LongAdder----------------------------------
    //1. 这是计算总和的,即将所有Cell里面的元素和base一起加起来。由于计算时没有对Cell数组加锁,所以累加过程中其他线程可能修改Cell的值或对数组扩容,所以sum返回的值不精确,并不是一个原子快照
    long sum()
    //2. 把base置0,并把Cell数组内有元素的都重置为0
    void reset()
    //3. 就是sum()和reset()合起来,求和的过程中重置。由于也没有加锁,该方法在多线程环境下会有问题,比如一个线程执行了这个方法,而另一个线程紧随着也执行这个方法则总会返回0
    long sumThenReset()
    //4. 等价于sum()
    longValue()
    //5. 这个方法是加一个数,看一下它是如何执行的
    add()

7.3 LongAccumulator


7.4 伪共享问题

  • 什么是伪共享?什么是缓存失效?
    • Thread1和Thread2都需要对Cell[]数组作修改,Thread1修改Cell[0],Thread2修改Cell[1]
    • 但由于Cell[0]和Cell[1]两个共享变量在同一个缓存行中(Cell数组元素的内存地址是连续的,所以数组内的多个元素能经常共享缓存行)
    • 如果Thread1修改Cell[0],会导致Thread1的缓存行失效(MESI协议中,缓存行会有个status,通过status可以判断自己失效了),Thread1需要重新刷新缓存
    • 这样一来,Thread1和Thread2修改Cell[]时,每次会让对方刷新缓存,缓存的作用就体现不出来了(本来指望通过缓存加载更快)
    • 这就是伪共享问题,看上去通过缓存共享了数据,实际相互一直让对方的缓存失效
  • 注:最常见的缓存行大小是64个字节
  • Java的解决方案
    • 传统方案:VolatileLong类型,原理是自动补齐(Padding)至一整个缓存行的大小
    • Java8的方案:Java8中新增了一个注解:@sun.misc.Contended。加上这个注解的类会自动补齐缓存行,需要注意的是此注解默认是无效的,需要在jvm启动时设置-XX:-RestrictContended才会生效
  • Reference:https://blog.csdn.net/hanmindaxiongdi/article/details/81159314

7.5 Unsafe对象

  • 包路径:sun.misc.Unsafe
  • 介绍:都是本地native方法,不要轻易使用(从JDK9开始,Java的新模块化设计将使得非标准库的模块都无法访问到sun.misc.Unsafe)
  • 常用的native方法
    • synchronized相关
      • public native void monitorEnter(Object var1);
      • public native void monitorExit(Object var1);
    • 异常
      • public native void throwException(Throwable var1);
    • CAS相关
      • public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
      • public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
      • public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
    • 线程
      • public native void park(boolean var1, long var2);
      • public native void unpark(Object var1);
  • 使用Unsafe对象的方法
    • 暴力反射

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值