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


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

1. AtomicLong类

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

​ 以下都是以AtomicLong类为例

(1). 递增和递减操作

// 自增,然后获取值
public final long incrementAndGet() {
    return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
}

// 自减,然后获取值
public final long decrementAndGet() {
    return unsafe.getAndAddLong(this, valueOffset, -1L) - 1L;
}

// 先获取值,然后自增
public final long getAndIncrement() {
        return unsafe.getAndAddLong(this, valueOffset, 1L);
    }

// 先获取值,然后自减
public final long getAndDecrement() {
    return unsafe.getAndAddLong(this, valueOffset, -1L);
}

​  这些方法内部都是通过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类中将一个变量分解为多个变量,让每个线程都可以有线程可以操作,解决了性能问题.

在这里插入图片描述

扫描二维码关注公众号,回复: 9177399 查看本文章

​  在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的初始值,可以自定义计数规则.

发布了141 篇原创文章 · 获赞 47 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/qq_41596568/article/details/104055204
今日推荐