原子操作类
volatile解决多线程内存不可见的问题对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题。
在JDK8中,推荐使用LongAdder对象,比AtomicLong性能更好(减少乐观锁的重试次数)
基本类型原子类
- AtomicInteger
- AtomicBoolean
- AtomicLong
常用API
get():获取当前的值
getAndSet(int newValue):获取当前值,并设置新值
getAndIncrement():获取当前值,并自增
getAndDecrement():获取当前值,并自减
getAndAdd(int delta):获取当前值,并加上预期的值
cpmpareAndSet(int expect, int update):如果输入的数值等于预期值,则以原子方式将该值设置为输入值。
使用CountDownLatch来避免在多线程情况下由于其他线程未完成任务,主线程就get值的情况。
数组类型原子类
- AtomicIntegerArray
- AtomicLongArray
- AtomicReferenceArray
使用方法和基本类型原子类类似
引用类型原子类
- AtomicReference
- AtomicStampedReference:使用版本号可以解决修改过几次的问题。
- AtomicMarkableReference:使用标记位可以解决是否被修改过的问题。
对象的属性修改原子类
- AtomicReferenceFieldUpdater
- AtomicLongFieldUpdater
- AtomicIntegerFieldUpdater:基于反射的实用程序,可以对指定类的制定volatile int 字段进行原子更新
使用目的:以一种线程安全的方式操作非线程安全对象内的某些字段
要求:1. 更新的对象属性必须使用public volatile 修饰符。2. 因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。
原子操作增强类远离深度解析
- DoubleAccumulator
- DoubleAdder
- LongAccumulator
- LongAdder
案例:1. 商品点赞计数器,不要求实时准确。2. 一个很大的List,里面都是int类型,如何实现++。
使用场景:
- LongAdder只能用来计算加法,且从0开始计算。
- LongAccumulator提供了自定义的函数操作,用来扩展。
在高并发的环境下,LongAdder的性能高于AtomicLong。
LongAdder源码分析
LongAdder继承自Striped64,再继承自Number
Cell时Striped64的一个内部类
Striped64中的重要变量:
- NCPU:cpu核数,即cells数组最大长度
- cells:cell的数组,2的幂
- base: 基础的value值
- cellsBusy:创建或者扩容Cells时使用的自旋锁变量调整单元格大小。
LongAdder的基本思路就是分散热点,将value值分散到一个Cell数组中。不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样 热点被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。
sum()会将所有Cell数组中的value和base累加作为返回值,核心思想就是将之前的AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点。
LongAdder再无竞争的情况,跟AtomicLong一样,对同一个base进行操作,当出现竞争关系时则是采用化整为零分散热点的做法,用空间换时间,用一个数组cells,将一个value拆分进这个数组cells。多个线程需要同时对value进行操作时,可以对线程id进行hash得到hash值,再根据hash值映射到这个数组cells的某个下标,再对该下标所对应的值进行自增操作。当所有线程操作完毕后,将数组cells的所有值和base都加起来作为最终结果。
add(1L)
步骤:
- 如果Cells表为空,尝试用CAS更新base字段,成功则推出。
- 如果Cells表为空,CAS更新base字段失败,出现竞争,uncontended为true,调用longAccumulate。(初始化Cell[])
- 如果Cells表非空,但是当前线程映射的Cells槽为空,uncontended为true,调用longAccumulate。(初始化某个Cell)
- 如果Cells表非空,且当前线程映射的Cells槽为空,CAS更新Cell的值,成功则返回,否则,uncontended设为false,调用longAccumulate。(扩容Cell[])
longAccumulate
三个入参:
- long x:需要增加的值,一般默认都是1
- LongBinaryOperator fn 默认传递nul
- wasUncontended竞争标识,如果是false则代表有竞争。只有cells初始化后,并且当前线程CAS竞争失败才会使false
Striped64中的一些变量和方法:
步骤:
首先给当前线程分配一个hash,然后进入自旋,其中自选分为三个分支:
1. Cell[]已经初始化,且可能存在Cell数组扩容:
(1)
(2)
(3)
(4)
(5)
(6)
- Cell[]未初始化(首次新建):未初始化过Cell[]。尝试占有锁并首次初始化cell[]
- Cell[]正在初始化中:多个线程尝试CAS修改失败的线程会走该分支,尝试直接在base上累加
总结
:
sum
sum = base+Cell[]
为什么再并发的环境下sum可能会不准确:sum执行时,并没有限制对base和cells的更新。所以LongAdder不是强一致性,而是最终一致性的。
首先,最终返回的sum局部变量,初始被赋值为base,而最终返回时,很可能base已经被修改了,而此时局部变量sum不会更新,造成不一致。其次,这里对cell的读取也无法保证时最后一次写入的值。所以,sum再没有并发的情况下,才可以确定获得准确的结果。
使用场景
AtomicLong:
- 线程安全,可允许一些性能消耗,要求高精度时可以使用。
- 保证精度,有性能代价。
- AtomicLong是多个线程针对单个热点值value进行原子操作。
LongAdder:
- 当需要在高并发下有较好的性能表现,且对精度要求不高时使用。
- 保证性能,有精度代价。
- LongAdder是每个线程拥有自己的槽,各个线程一般只对自己槽中的那个值进行CAS操作。
总结
AtomicLong:
原理:CAS+自旋
主要方法:incrementAndGet
场景:低并发下的全局计算,能保证并发情况下计数的准确性,其内部通过CAS来解决并发安全性的问题
缺陷:高并发后性能急剧下降,因为自旋会成为瓶颈
LongAdder:
原理:CAS+Base+Cell[],空间换时间
场景:高并发下的全局计算
缺陷:sum求和后还有计算线程修改结果的话,最终结果不够准确
CAS+自旋
主要方法:incrementAndGet
场景:低并发下的全局计算,能保证并发情况下计数的准确性,其内部通过CAS来解决并发安全性的问题
缺陷:高并发后性能急剧下降,因为自旋会成为瓶颈
LongAdder:
原理:CAS+Base+Cell[],空间换时间
场景:高并发下的全局计算
缺陷:sum求和后还有计算线程修改结果的话,最终结果不够准确