Java atomic原子操作类的介绍

基于JDK1.8详细介绍了JUC下面的atomic子包中的大部分原子类的底层源码实现,比如AtomicInteger、AtomicIntegerArray、AtomicStampedReference等原子类源码。最后还介绍了JDK1.8对原子类的增强,比如LongAdder和LongAccumulator的原理!

1 atomic的概述

JDK1.5之前,为了保证Java中对单个变量的多个独立操作的原子性和安全性,通常会使用到synchronized锁,但是synchronized需要底层操作系统mutex资源的支持,这是一种重量级资源,性能比较低!

JDK1.5的时候,新增了JUC包,增加了许多和同步有关的特性,大大提高了使用Java进行并发编程的效率,比如并发集合、并发队列、新lock锁等。另外,JUC包下面还提供了一个java.util.concurrent.atomic子包,这个atomic包中的类用于在多线程环境下实现单个变量多个独立操作(比如读-写)的连续原子性,并且都比较高效,因为它们都是由基于偏移量(类似于指针)的非阻塞CAS算法实现,用于替代锁的使用。

JDK1.8的atomic包中具有17个原子类,根据支持的更新变量的类型,我们可以对常用原子类分为三种,分别是原子更新单个变量、原子更新数组、原子更新引用属性(字段)。

atomic 包下的常用原子类如下:

摘要
AtomicBoolean用原子方式更新的 boolean 值。
AtomicInteger用原子方式更新的 int 值。
AtomicLong用原子方式更新的 long 值。
AtomicReference< V > 用原子方式更新的对象引用。
AtomicMarkableReference< V > 维护带有boolean标志位的对象引用,可以原子方式对其进行更新。
AtomicStampedReference< V > 维护带有int整数版本号的对象引用,可用原子方式对其进行更新。
AtomicIntegerArray用原子方式更新其元素的 int 数组。
AtomicLongArray用原子方式更新其元素的 long 数组。
AtomicReferenceArray< E > 用原子方式更新其元素的对象引用数组。
AtomicIntegerFieldUpdater< T > 基于反射的实用工具,可以对指定类的指定非私有非静态的 volatile int 字段进行原子更新。
AtomicLongFieldUpdater< T > 基于反射的实用工具,可以对指定类的指定非私有非静态的 volatile long 字段进行原子更新。
AtomicReferenceFieldUpdater< T,V > 基于反射的实用工具,可以对指定类的指定非私有非静态的 volatile 引用字段进行原子更新 。
LongAdderJDK1.8新增加的原子类累加器,使用热点数据分离的思想对long数据进行加法运算,性能更佳!
LongAccumulatorJDK1.8新增加的原子类累加器,使用热点数据分离的思想对long数据进行指定规则的运算,性能更佳!
DoubleAdderJDK1.8新增加的原子类累加器,使用热点数据分离的思想对double数据进行加法运算,性能更佳!
DoubleAccumulatorJDK1.8新增加的原子类累加器,使用热点数据分离的思想对double数据进行指定规则的运算,性能更佳!

实际上Java中atomic包下的原子类的基石就是:volatile字段修饰符+CAS算法(Unsafe提供)。本文没有对这两个基本知识点做深入讲解,因为前面的文章中已经讲了,都是深入到了虚拟机源码级别,如果想要深入了解原子类的原理,应该要看看以下文章:Java中的volatile实现原理深度解析以及应用Java中的CAS实现原理深度解析与应用案例

2 原子更新单个变量

2.1 基本原子类

通过原子的方式更新单个变量,Atomic包提供了以下4个基础类:

1. AtomicBoolean:用原子方式更新的 boolean 值。
2. AtomicInteger:用原子方式更新的 int 值。
3. AtomicLong:用原子方式更新的 long 值。
4. AtomicReference< V >:用原子方式更新的对象引用。

上面四个原子类的原理几乎一致,我们以AtomicInteger来讲解。

AtomicInteger源码解析:Java AtomicInteger和AtomicStampedReference源码深度解析

2.2 带版本号的原子类

通过原子的方式更新单个变量的原子类的升级版,Atomic包提供了以下2个类:

1. AtomicMarkableReference< V >:维护带有标记位的对象引用,可以原子方式对其进行更新。
2. AtomicStampedReference< V >:维护带有整数标志的对象引用,可用原子方式对其进行更新。

上面两个原子类的方法以及原理几乎一致,属于带有版本号的原子类。我们知道CAS操作的三大问题之一就是“ABA”问题:CAS在操作值的时候,需要检查预期值有没有发生变化,如果没有发生变化则更新。但是,如果一个线程t1首先获取了预期值A,此时另一个线程t2则将值从A变成了B,随后又变成了A,随后t1再使用CAS进行比较交换的时候,会发现它的预期值“没有变化”,但实际上是变化过的。这就是ABA问题的由来。

ABA问题的解决思路就是使用版本号,1A->2B->3A,在Atomic包中,提供了一个现成的AtomicStampedReference类来解决ABA问题,使用的就是添加版本号的方法。还有一个AtomicMarkableReference实现类,它比AtomicStampedReference更加简单,AtomicStampedReference中每更新一次数据版本号也会更新一次,这样可以使用版本号统计到底更新了多少次,而AtomicMarkableReference仅仅使用了一个boolean值来表示值是否改变过,因此使用的比较少。

这里我们以AtomicStampedReference来讲解。

AtomicStampedReference源码解析:Java AtomicInteger和AtomicStampedReference源码深度解析

3 原子更新数组

通过原子的方式更新数组里的某个元素,Atomic包提供了以下3个类:

1. AtomicIntegerArray:用原子方式更新其元素的 int 数组。
2. AtomicLongArray:用原子方式更新其元素的 long 数组。
3. AtomicReferenceArray< E >:用原子方式更新其元素的对象引用数组。

上面三个原子类的原理几乎一致,我们以AtomicIntegerArray来讲解。

3.1 重要属性

可以看到内部就是一个int的数组,然后调用Unsafe的方法对数组的元素进行操作。

/**
 * 使用Unsafe操作数组
 */
private static final Unsafe unsafe = Unsafe.getUnsafe();
/**
 * 返回数组类型的第一个元素的偏移地址(基础偏移地址)。
 * 如果arrayIndexScale方法返回的比例因子不为0,你可以通过结合基础偏移地址和比例因子访问数组的所有元素。
 */
private static final int base = unsafe.arrayBaseOffset(int[].class);
/**
 * scale最高位的1的所在位数(从左从0开始),在计算某个索引的偏移量的时候
 * 使用是该值进行位运算而不是scale进行传统乘法运算,提升效率
 */
private static final int shift;
/**
 * 底层int数组
 */
private final int[] array;

static {
    //返回数组单个元素的大小,数组中的元素的地址是连续的,64位虚拟机应该是4
    int scale = unsafe.arrayIndexScale(int[].class);
    //大小必须是2的幂次方
    if ((scale & (scale - 1)) != 0)
        throw new Error("data type scale not a power of two");
    //numberOfLeadingZeros用于返回scale的最高非零位前面的0的个数,包括符号位在内;
    //31减去scale的最高非零位前面的0的个数,就表示scale最高位的1的所在位数,比如scale为2,那么shift为1,如果scale为4,那么shift为2
    shift = 31 - Integer.numberOfLeadingZeros(scale);
}

/**
 * 某个数组索引位置的元素的偏移量
 *
 * @param i 数组索引
 * @return 该索引的偏移量
 */
private long checkedByteOffset(int i) {
    if (i < 0 || i >= array.length)
        throw new IndexOutOfBoundsException("index " + i);
    return byteOffset(i);
}

/**
 * @param i 索引位置
 * @return 返回某个数组索引位置的元素的偏移量
 */
private static long byteOffset(int i) {
    //这里就能明白shift的作用了,对于2的幂次方的scale:
    //这里可以使用scale的最高为1的位置shift的位运算i << shift,代替scale*i的传统运算,效率提高
    //比如scale=4,那么shift=2,如果i=3,那么i<<shift = 3 << 2 = 12 就等于 scale*i = 4 * 3  = 12
    //比如scale=8,那么shift=3,如果i=3,那么i<<shift = 3 << 3 = 24 就等于 scale*i = 8 * 3  = 24
    return ((long) i << shift) + base;
}


/**
 * 创建给定长度的新 AtomicIntegerArray。
 *
 * @param length 给定长度
 */
public AtomicIntegerArray(int length) {
    array = new int[length];
}

/**
 * 创建与给定数组具有相同长度的新 AtomicIntegerArray,并从给定数组复制其所有元素。
 *
 * @param array 给定数组
 * @throws NullPointerException 如果数组为 null
 */
public AtomicIntegerArray(int[] array) {
    // 克隆数组,元素浅克隆
    this.array = array.clone();
}

3.2 重要方法

其常用方法如下,基于Unsafe的volatile和CAS操作:

/**
 * 获取i索引位置的当前值
 *
 * @param i 多赢
 * @return 当前值
 */
public final int get(int i) {
    return getRaw(checkedByteOffset(i));
}

private int getRaw(long offset) {
    //volatile的获取最新值
    return unsafe.getIntVolatile(array, offset);
}

/**
 * 在i索引位置设定为指定新值
 *
 * @param i        索引
 * @param newValue 新值
 */
public final void set(int i, int newValue) {
    //volatile的写
    unsafe.putIntVolatile(array, checkedByteOffset(i), newValue);
}


/**
 * 以原子方式将元素设置在i索引位置,并返回旧值
 *
 * @param i        索引
 * @param newValue 新值
 * @return 旧值
 */
public final int getAndSet(int i, int newValue) {
    return unsafe.getAndSetInt(array, checkedByteOffset(i), newValue);
}

/**
 * 以原子方式将输入值与数组中索引i的元素相加,并返回旧值
 *
 * @param i     索引
 * @param delta 相加的数据
 * @return 旧值
 */
public final int getAndAdd(int i, int delta) {
    return unsafe.getAndAddInt(array, checkedByteOffset(i), delta);
}

/**
 * 以原子方式将输入值与数组中索引i的元素相加,并返回新值
 *
 * @param i     索引
 * @param delta 相加的数据
 * @return 更新后的值
 */
public final int addAndGet(int i, int delta) {
    return getAndAdd(i, delta) + delta;
}


/**
 1. 如果当前值等于预期值,则以原子方式将数组位置i的元素设置成新值。
 2.  3. @param i      索引
 3. @param expect 预期值
 4. @param update 新值
 5. @return true表示CAS成功 false 表示CAS失败
 */
public final boolean compareAndSet(int i, int expect, int update) {
    return compareAndSetRaw(checkedByteOffset(i), expect, update);
}

4 原子更新字段属性

通过原子的方式更新对象里的某个字段,Atomic包提供了以下3个类:

1. AtomicIntegerFieldUpdater< T >:基于反射的实用工具,可以对指定类的指定非私有的 volatile int
   字段进行原子更新。
2. AtomicLongFieldUpdater< T >:基于反射的实用工具,可以对指定类的指定非私有的 volatile long
   字段进行原子更新。
3. AtomicReferenceFieldUpdater< T,V >:基于反射的实用工具,可以对指定类的指定非私有的 volatile
   引用字段进行原子更新。

以上3个类的原理几乎一样,我们以AtomicIntegerFieldUpdater来讲解。

AtomicIntegerFieldUpdater实际上是一个抽象类,它的实现类实际上在它的内部而且是私有的,因此只能使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性字符串名。

另外,这里对于对象的字段的设置是先采用getDeclaredField方法反射获取的对应字段的Filed对象,然后在对Filed对象进行操作,并且没有设置setAccessible权限,因此类的字段属性不能是私有属性!

由于CAS 操作会通过对象实例中的偏移量直接进行赋值,即Unsafe. objectFieldOffset()方法。因此,它不支持对static属性的赋值。

对象的字段还应该被设置为volatile类型,这样就能获取到最新的值。

/**
 1. @author lx
 */
public class AtomicFieldUpdaterTest {
    public static void main(String[] args) {
        AtomicIntegerFieldUpdater<User> old = AtomicIntegerFieldUpdater.newUpdater(User.class, "old");
        AtomicReferenceFieldUpdater<User, String> name = AtomicReferenceFieldUpdater.newUpdater(User.class, String.class, "name");

        User user = new User("user", 10);

        System.out.println(old.getAndIncrement(user));
        System.out.println(old.get(user));

        System.out.println(name.getAndSet(user, "user2"));
        System.out.println(name.get(user));
    }

    public static class User {
        volatile String name;
        volatile int old;

        User(String name, int old) {
            this.name = name;
            this.old = old;
        }

        public String getName() {
            return name;
        }

        public int getOld() {
            return old;
        }
    }
}

5 原子类的加强

JDK1.8的时候,新增了四个原子类:

1. LongAdder:long类型的数值累加器,从0开始累加,累加规则为加法运算。
2. LongAccumulator:long类型的数值累加器,可从指定值开始累加,可指定累加规则。
3. DoubleAdder:double类型的数值累加器,从0开始累加,累加规则为加法运算。
4. DoubleAccumulator:double类型的数值累加器,可从指定值开始累加,可指定累加规则。

自从原子类问世之后,多线程环境下如果用于统计计数操作,一般可以使用AtomicLong来代替锁作为计数器,AtomicLong 通过CAS 提供了非阻塞的原子性操作,相比使用阻塞算法的同步器来说它的性能己经很好了,那么,它们有什么缺点吗?

实际上,AtomicLong等其他传统的atomic原子类对于数值的更改,通常都是在一个无限循环(自旋)中不断尝试CAS 的修改操作,一旦CAS失败则循环重试,这样来保证最终CAS操作成功。如果竞争不激烈,那么修改成功的概率就很高,但是如果在高并发下大量线程频繁的竞争修改计数器,会造成一次CAS修改失败的概率就很高。在大量修改失败时,这些原子操作就会进行多次循环尝试,白白浪费CPU 资源,因此性能还是会受到影响。

JDK1.8新增这些类,正是为了解决高并发环境下由于频繁读写AtomicLong等计数器而可能造成某些线程持续的空转(循环)进而浪费CPU的情况,它们也被称为“累加器”!

LongAdder和DoubleAdder,LongAccumulator和DoubleAccumulator的原理差不多。实际上DoubleAdder中对于double的累加也是先通过Double.doubleToRawLongBits将double类型转换为long类型来进行计算的,并且底层也是存储的long类型的值,在获取总和的时候又会通过Double.longBitsToDouble将存储的long值转换为double。

下面我们将对LongAdder和LongAccumulator进行讲解!

LongAdder:Java LongAdder原子加法器源码深度解析

LongAccumulator:Java LongAccumulator原子累加器源码深度解析

6 atomic的总结

JDK1.5出现的atomic包下面的原子类,在对于单个变量的复合操作(比如读-写)中可以代替锁的来保证操作的原子性和安全性,并且由于没有使用锁而有不错的性能,但是对于多个变量的复合操作以及一批代码的原子性和安全性却无能为力,此时只能使用锁。

我们可以看到,实际上volatile关键字以及Unsafe类提供的CAS的方法就是构成原子类的基石,原子类的方法实际上就是对于Unsafe中的CAS方法的二次包装,方法开发人员使用而已。Unsafe中的CAS方法作为native方法,本身并不是Java语言实现的,它们的源码位于JVM虚拟机的源码中,HotSpot虚拟机的源码中就有这些native方法的具体实现,它们都是采用C++的代码实现的,方便与底层系统交互,在openjdk中可以找到。

本文没有对一些基本知识点做深入讲解,比如Unsafe、volatile、CAS、伪共享、JMH等,因为前面的文章中已经讲了,都是深入到了虚拟机源码级别,如果想要深入了解原子类的原理,应该要看看以下文章!

相关文章:

  1. Unsafe:JUC—Unsafe类的原理详解与使用案例
  2. volatile:Java中的volatile实现原理深度解析以及应用
  3. CAS:Java中的CAS实现原理深度解析与应用案例
  4. 伪共享:Java中的伪共享深度解析以及避免方法
  5. JMH:Java使用JMH进行方法性能优化测试

如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

  • 6
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

刘Java

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值