Java并发编程--原子操作类原理剖析

绝地灬酷狼 2023-07-01 08:53 105阅读 0赞

文章目录

    1. AtomicLong类
      • (1). 递增和递减操作
      • (2). boolean compareAndSet(long expect,long update)方法
    1. LongAdder类
    • 2.1 介绍
    • 2.2 LongAdder代码分析
      • (1). LongAdder的结构
      • (2). 线程应访问数组中的哪个Cell元素?
      • (3). 初始化Cell数组
      • (4). 何时进行Cell数组的扩容
      • (5). 分配的Cell的冲突处理
      • (6). 保证被分配的Cell的原子性
    1. LongAccumulator类原理探究

 JUC包(java.util.concurrent)提供了一系列原子性操作类,这些类都是使用非阻塞算法CAS实现的,相比使用锁实现原子性操作在性能上有很大提高.

1. AtomicLong类

 JUC并发包包含有AtomicInteger,AtomicLong,AtomicBoolean等原子性操作类,原理都是CAS算法.

以下都是以AtomicLong类为例

(1). 递增和递减操作

  1. // 自增,然后获取值
  2. public final long incrementAndGet() {
  3. return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
  4. }
  5. // 自减,然后获取值
  6. public final long decrementAndGet() {
  7. return unsafe.getAndAddLong(this, valueOffset, -1L) - 1L;
  8. }
  9. // 先获取值,然后自增
  10. public final long getAndIncrement() {
  11. return unsafe.getAndAddLong(this, valueOffset, 1L);
  12. }
  13. // 先获取值,然后自减
  14. public final long getAndDecrement() {
  15. return unsafe.getAndAddLong(this, valueOffset, -1L);
  16. }

 这些方法内部都是通过Unsafe的getAndAddLong方法来实现操作,原操作返回的都是修改之前的值.

(2). boolean compareAndSet(long expect,long update)方法

 功能:如果当前值==except,则更新值为update,返回true,否则返回false

 直接调用了unsafe.compareAndSwapLong()方法.

 如果变量的值等于expect,替换为update并返回true,否则返回flase.

2. LongAdder类

2.1 介绍

 使用AtomicLong时,高并发情况下,由于CAS操作的缺陷,会导致大量线程同时竞争更新一个原子变量,会有大量线程竞争失败后无限循环进行自旋尝试
在这里插入图片描述

LongAdder类中将一个变量分解为多个变量,让每个线程都可以有线程可以操作,解决了性能问题.

在这里插入图片描述

 在LongAdder类中,内部维护了多个Cell变量和一个base值.每一个Cell里都有一个初始值为0的long型变量,多个线程竞争同一个Cell原子变量使如果失败了,不会一直自旋式的重试,而是试图在其他Cell上进行CAS尝试.

 Cell占用的内存相对较大,在使用时才会创建,也就是惰性加载.

 一开始所有的累加操作都是对base变量进行的,当并发大了之后,初始化Cell数组,并保存Cell数量为2^n(初次为2)

 原子性数组的内存是连续的,所以要使用@sun.misc.Contended注解对Cell类记性字节填充,防止了多个数组中多个元素共享一个缓存行

2.2 LongAdder代码分析

(1). LongAdder的结构

在这里插入图片描述

 LongAdder类继承自Striped64类,其内部维护着三个变量:base,cells[]和cellsBusy.前两个变量用来记性计数,第三个变量用来实现自旋锁,只有0和1两个值.

 Cell的构造很简单,内部维护一个被声明为volatile的变量(保证可见性),通过CAS操作进行更新(保证原子性)

1). long sum()

 返回当前的计数和.返回的是调用时的快照,多线程情况下是可能对其改变的.所以LongAdder并不能完全代替LongAtomic.

2). void reset()

 重置操作,将base置为零,如果Cell数组有元素,全部重置为0.

3). long sumThenReset

 sum的改造版本,返回计数和,并重置.

 多线程下会有问题,一个线程将值置为0,那么其他线程就是在0上面进行计数了.

4). long longValue()

 等价于sum

5). void add(long x)

 如果Cell数组为空,则在base上进行计数,如果Cell不为空或者CAS操作失败了,获取当前线程应该访问的cells数组中的Cell元素(使用 getProbe()&(cells.length-1) 计算其下标).如果cells的元素个数小于当前机器CPU的个数,并且当前出现了CAS错误,就会进行cells数组的扩容.

getProbe()用于获取当前线程中变量threadLocalRandomProbe的值,可以看作是线程的hash值,CAS失败线程会重新计算这个值,以减少下次冲突的可能性.

6). void longAccumulate(…)

 这个方法用来进行cells数组的初始化和扩容.

 在进行扩容或者初始化时,会先将cellsBusy设置为1,当当前线程开始进行方法内部逻辑时,别的线程不能进行扩容.不使用CAS,因为cellsBusy变量是声明为volatile类型的.

扩容时进行的操作是建立一个2被于原来容量的数组,将原数组的值复制进去.

(2). 线程应访问数组中的哪个Cell元素?

 通过getProbe()&m计算得到的。

 getProbe()用于获取当前线程中变量threadLocalRandomProbe的值

 m是当前cells数组元素个数-1

(3). 初始化Cell数组

  1. 首先通过CAS设置cellBusy为1(表示有线程对其初始化或扩容操作)
  2. 初始化cell数组元素个数为2
  3. 使用getProbe()&1计算当前线程应访问的数组位置
  4. 标示cell数组被初始化
  5. 重置cellBusy标记

(4). 何时进行Cell数组的扩容

 当前cells的元素个数<当前机器CPU个数,并且当前多个线程访问了cells中同一个元素,从而导致冲突使其中一个线程CAS失败时才会进行扩容

(5). 分配的Cell的冲突处理

 对CAS失败的线程重新计算当前线程的随机值threadLocalRandomProbe,以减少下次访问cells元素时冲突的机会

(6). 保证被分配的Cell的原子性

 通过CAS操作,保证当前线程更新时被分配的Cell元素中value值的原子性

3. LongAccumulator类原理探究

 LongAdder类是LongAccumulator类的一个特例,后者比前者功能更强大.可以提供非0的初始值,可以自定义计数规则.

发表评论

表情:
评论列表 (有 0 条评论,105人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Java原子操作原理剖析

    CAS的概念 ◆ 对于并发控制来说,使用锁是一种悲观的策略。它总是假设每次请求都会产生冲突,如果多个线程请求同一个资源,则使用锁宁可牺牲性能也要保证线程安全。而无...

    相关 Java并发编程原子

    1. 并发编程概念 原子性 一个操作不能被再拆分了;即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。一个很经典的例子就是银行账