Java原子操作类

  • Post author:
  • Post category:java




原子操作类

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类型,如何实现++。

使用场景:

  1. LongAdder只能用来计算加法,且从0开始计算。
  2. LongAccumulator提供了自定义的函数操作,用来扩展。

在高并发的环境下,LongAdder的性能高于AtomicLong。



LongAdder源码分析

LongAdder继承自Striped64,再继承自Number

Cell时Striped64的一个内部类

Striped64中的重要变量:

  1. NCPU:cpu核数,即cells数组最大长度
  2. cells:cell的数组,2的幂
  3. base: 基础的value值
  4. 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)

image.png

步骤:

  1. 如果Cells表为空,尝试用CAS更新base字段,成功则推出。
  2. 如果Cells表为空,CAS更新base字段失败,出现竞争,uncontended为true,调用longAccumulate。(初始化Cell[])
  3. 如果Cells表非空,但是当前线程映射的Cells槽为空,uncontended为true,调用longAccumulate。(初始化某个Cell)
  4. 如果Cells表非空,且当前线程映射的Cells槽为空,CAS更新Cell的值,成功则返回,否则,uncontended设为false,调用longAccumulate。(扩容Cell[])



longAccumulate

三个入参:

  1. long x:需要增加的值,一般默认都是1
  2. LongBinaryOperator fn 默认传递nul
  3. wasUncontended竞争标识,如果是false则代表有竞争。只有cells初始化后,并且当前线程CAS竞争失败才会使false

Striped64中的一些变量和方法:


image.png

步骤:

​ 首先给当前线程分配一个hash,然后进入自旋,其中自选分为三个分支:

	1. Cell[]已经初始化,且可能存在Cell数组扩容:

​ (1)
image.png

​ (2)
image.png

​ (3)
image.png

​ (4)
image.png

​ (5)
image.png

​ (6)
image.png

  1. Cell[]未初始化(首次新建):未初始化过Cell[]。尝试占有锁并首次初始化cell[]

image.png

  1. Cell[]正在初始化中:多个线程尝试CAS修改失败的线程会走该分支,尝试直接在base上累加


总结

image.png



sum

sum = base+Cell[]

为什么再并发的环境下sum可能会不准确:sum执行时,并没有限制对base和cells的更新。所以LongAdder不是强一致性,而是最终一致性的。

首先,最终返回的sum局部变量,初始被赋值为base,而最终返回时,很可能base已经被修改了,而此时局部变量sum不会更新,造成不一致。其次,这里对cell的读取也无法保证时最后一次写入的值。所以,sum再没有并发的情况下,才可以确定获得准确的结果。



使用场景

AtomicLong:

  1. 线程安全,可允许一些性能消耗,要求高精度时可以使用。
  2. 保证精度,有性能代价。
  3. AtomicLong是多个线程针对单个热点值value进行原子操作。

LongAdder:

  1. 当需要在高并发下有较好的性能表现,且对精度要求不高时使用。
  2. 保证性能,有精度代价。
  3. LongAdder是每个线程拥有自己的槽,各个线程一般只对自己槽中的那个值进行CAS操作。



总结

AtomicLong:

​ 原理:CAS+自旋

​ 主要方法:incrementAndGet

​ 场景:低并发下的全局计算,能保证并发情况下计数的准确性,其内部通过CAS来解决并发安全性的问题

​ 缺陷:高并发后性能急剧下降,因为自旋会成为瓶颈

LongAdder:

​ 原理:CAS+Base+Cell[],空间换时间

​ 场景:高并发下的全局计算

​ 缺陷:sum求和后还有计算线程修改结果的话,最终结果不够准确

CAS+自旋

​ 主要方法:incrementAndGet

​ 场景:低并发下的全局计算,能保证并发情况下计数的准确性,其内部通过CAS来解决并发安全性的问题

​ 缺陷:高并发后性能急剧下降,因为自旋会成为瓶颈

LongAdder:

​ 原理:CAS+Base+Cell[],空间换时间

​ 场景:高并发下的全局计算

​ 缺陷:sum求和后还有计算线程修改结果的话,最终结果不够准确



版权声明:本文为qq_37032116原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。