• 盘点JDK中基于CAS实现的原子类


    前言

    JDK中提供了一系列的基于CAS实现的原子类,CAS 的全称是Compare-And-Swap,底层是lock cmpxchg指令,可以在单核和多核 CPU 下都能够保证比较交换的原子性。所以说,这些原子类都是线程安全的,而且是无锁并发,线程不会频繁上下文切换,所以在某些场景下性能是优于加锁。

    本文就盘点一下JDK中的原子类,方便我们后续拿来使用。

    基础原子类

    • AtomicInteger:Integer整数类型的原子操作类
    • AtomicBoolean:Boolean类型的原子操作类
    • AtomicLong:Long类型的原子操作类

    这边以AtomicInteger讲解下它的API和用法。

    构造方法:

    • public AtomicInteger():初始化一个默认值为 0 的原子型 Integer
    • public AtomicInteger(int initialValue):初始化一个指定值的原子型 Integer

    常用API:

    • public final int get(): 获取 AtomicInteger 的值
    • public final int getAndIncrement(): 以原子方式将当前值加 1,返回的是自增前的值
    • public final int incrementAndGet():以原子方式将当前值加 1,返回的是自增后的值
    • public final int getAndSet(int value):以原子方式设置为 newValue 的值,返回旧值
    • public final int addAndGet(int data):以原子方式将输入的数值与实例中的值相加并返回

    使用:

    • 结果1000,大致说明并发情况下保证了线程安全

    原理分析:

    整体实现思路: 自旋(循环) + CAS算法

    • 当旧的预期值 A == 内存值 V 此时可以修改,将 V 改为 B
    • 当旧的预期值 A != 内存值 V 此时不能修改,并重新获取现在的最新值,重新获取的动作就是自旋
    1. public final int getAndIncrement() {
    2. return unsafe.getAndAddInt(this, valueOffset, 1);
    3. }
    4. 复制代码
    • valueOffset:偏移量表示该变量值相对于当前对象地址的偏移,Unsafe 就是根据内存偏移地址获取数据

    • 从主内存中拷贝到工作内存中的值(每次都要从主内存拿到最新的值到本地内存),然后执行 compareAndSwapInt() 再和主内存的值进行比较,假设方法返回 false,那么就一直执行 while 方法,直到期望的值和真实值一样,修改数据。

    • 原子类AtomicIntegervalue属性是volatile类型,保证了多线程之间的内存可见性,避免线程从工作缓存中获取失效的变量。

    原子引用

    原子引用主要是对对象的原子操作,原子引用类分为AtomicReferenceAtomicStampedReferenceAtomicMarkableReference。它们之间有什么区别呢?

    1. AtomicReference类

    普通的原子类对象

    1. public class AtomicReferenceDemo {
    2. public static void main(String[] args) {
    3. User user1 = new User("旭阳");
    4. // 创建原子引用包装类
    5. AtomicReference<User> atomicReference = new AtomicReference<>(user1);
    6. while (true) {
    7. User user2 = new User("alvin");
    8. // 比较并交换
    9. if (atomicReference.compareAndSet(user1, user2)) {
    10. break;
    11. }
    12. }
    13. System.out.println(atomicReference.get());
    14. }
    15. }
    16. @Data
    17. @AllArgsConstructor
    18. @ToString
    19. class User {
    20. private String name;
    21. }
    22. 复制代码
    • 调用compareAndSet()方法进行比较替换对象

    ABA问题

    但是如果使用AtomicReference类,会有一个ABA问题。什么意思呢?就是一个线程将共享变量从A改成B, 后面又改回A, 这是,另外一个线程就无法感知这个变化过程,就傻傻的比较,就以为没有变化,还是一开始的A,就替换了。 实际的确存在这样只要共享变量发生过变化,就要CAS失败,有什么办法呢?

    1. AtomicStampedReference类

    带版本号的原子类对象

    1. @Slf4j(topic = "a.AtomicStampedReferenceTest")
    2. public class AtomicStampedReferenceTest {
    3. // 构造AtomicStampedReference
    4. static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
    5. public static void main(String[] args) throws InterruptedException {
    6. log.debug("main start...");
    7. // 获取值 A
    8. String prev = ref.getReference();
    9. // 获取版本号
    10. int stamp = ref.getStamp();
    11. log.debug("版本 {}", stamp);
    12. // 如果中间有其它线程干扰,发生了 ABA 现象
    13. other();
    14. Thread.sleep(1000);
    15. // 尝试改为 C
    16. log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));
    17. }
    18. private static void other() throws InterruptedException {
    19. new Thread(() -> {
    20. log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B",
    21. ref.getStamp(), ref.getStamp() + 1));
    22. log.debug("更新版本为 {}", ref.getStamp());
    23. }, "t1").start();
    24. Thread.sleep(500);
    25. new Thread(() -> {
    26. log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A",
    27. ref.getStamp(), ref.getStamp() + 1));
    28. log.debug("更新版本为 {}", ref.getStamp());
    29. }, "t2").start();
    30. }
    31. }
    32. 复制代码

    • 虽然对象的值变回了A,但是由于版本变了,所以主线程CAS失败
    1. AtomicMarkableReference 类

    其实有时候并不关心共享变量修改了几次,而是只要标记下是否发生过更改,是否加个标记即可,所以就有了AtomicMarkableReference类。

    1. @Slf4j(topic = "c.AtomicMarkableReferenceTest")
    2. public class AtomicMarkableReferenceTest {
    3. // 构造 AtomicMarkableReference, 初始标记为false
    4. static AtomicMarkableReference<String> ref = new AtomicMarkableReference<>("A", false);
    5. public static void main(String[] args) throws InterruptedException {
    6. log.debug("main start...");
    7. other();
    8. Thread.sleep(1000);
    9. // 看看是否发生了变化
    10. log.debug("change {}", ref.isMarked());
    11. }
    12. private static void other() throws InterruptedException {
    13. new Thread(() -> {
    14. log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B",
    15. false, true));
    16. }, "t1").start();
    17. Thread.sleep(500);
    18. new Thread(() -> {
    19. log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A",
    20. true, true));
    21. }, "t2").start();
    22. }
    23. }
    24. 复制代码

    • 通过调用isMarked()方法查看是否发生变化。

    原子数组

    • AtomicIntegerArray: Integer类型的原子数组
    • AtomicLongArray:Long类型的原子数组
    • AtomicReferenceArray:引用类型的原子数组

    直接上例子

    1. public class AtomicIntegerArrayTest {
    2. public static void main(String[] args) throws Exception{
    3. AtomicIntegerArray array = new AtomicIntegerArray(10);
    4. Thread t1 = new Thread(()->{
    5. int index;
    6. for(int i=1; i<100000; i++) {
    7. index = i%10; //范围0~9
    8. array.incrementAndGet(index);
    9. }
    10. });
    11. Thread t2 = new Thread(()->{
    12. int index;
    13. for(int i=1; i<100000; i++) {
    14. index = i%10; //范围0~9
    15. array.decrementAndGet(index);
    16. }
    17. });
    18. t1.start();
    19. t2.start();
    20. Thread.sleep(5 * 1000);
    21. System.out.println(array.toString());
    22. }
    23. }
    24. 复制代码

    • 两个线程同时对数组对象进行加和减的操作,最终结果都是0,说明线程安全。

    原子字段更新器

    • AtomicReferenceFieldUpdater
    • AtomicIntegerFieldUpdater
    • AtomicLongFieldUpdater

    利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常。

    1. @Data
    2. public class AtomicReferenceFieldUpdaterTest {
    3. private volatile int age = 10;
    4. private int age2;
    5. public static void main(String[] args) {
    6. AtomicIntegerFieldUpdater integerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(AtomicReferenceFieldUpdaterTest.class, "age");
    7. AtomicReferenceFieldUpdaterTest ref = new AtomicReferenceFieldUpdaterTest();
    8. // 对volatile 的age字段+1
    9. integerFieldUpdater.getAndIncrement(ref);
    10. System.out.println(ref.getAge());
    11. // 修改 非volatile的age2
    12. integerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(AtomicReferenceFieldUpdaterTest.class, "age2");
    13. integerFieldUpdater.getAndIncrement(ref);
    14. }
    15. }
    16. 复制代码

    • 原子字段更新器只能更新volatile字段,它可以保证可见性,但是无法保证原子性。

    原子累加器

    原子累加器主要是用来做累加的,相关的类有LongAdderDoubleAdderLongAccumulatorDoubleAccumulator

    LongAdder是jdk1.8中引入的,它的性能要比AtomicLong方式好。

    LongAddr 类是 LongAccumulator 类的一个特例,只是 LongAccumulator 提供了更强大的功能,可以自定义累加规则,当accumulatorFunction 为 null 时就等价于 LongAddr

    这边做个性能的对比例子。

    1. public class LongAdderTest {
    2. public static void main(String[] args) {
    3. System.out.println("LongAdder ...........");
    4. for (int i = 0; i < 5; i++) {
    5. addFunc(() -> new LongAdder(), adder -> adder.increment());
    6. }
    7. System.out.println("AtomicLong ...........");
    8. for (int i = 0; i < 5; i++) {
    9. addFunc(() -> new AtomicLong(), adder -> adder.getAndIncrement());
    10. }
    11. }
    12. private static <T> void addFunc(Supplier<T> adderSupplier, Consumer<T> action) {
    13. T adder = adderSupplier.get();
    14. long start = System.nanoTime();
    15. List<Thread> ts = new ArrayList<>();
    16. // 40个线程,每人累加 50
    17. for (int i = 0; i < 40; i++) {
    18. ts.add(new Thread(() -> {
    19. for (int j = 0; j < 500000; j++) {
    20. action.accept(adder);
    21. }
    22. }));
    23. }
    24. ts.forEach(t -> t.start());
    25. ts.forEach(t -> {
    26. try {
    27. t.join();
    28. } catch (InterruptedException e) {
    29. e.printStackTrace();
    30. }
    31. });
    32. long end = System.nanoTime();
    33. System.out.println(adder + " cost:" + (end - start)/1000_000);
    34. }
    35. }
    36. 复制代码

    主要是由于LongAdder会设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加Cell[1]... 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能。

    总结

    本文总结了JDK中提供的各种原子类,包括基础原子类、原子引用类、原子数组类、原子字段更新器和原子累加器等。有时候,使用这些原子类的性能是比加锁要高的,特别是在读多写少的场景下。但是,不知道大家发现没有,所有的原子类操作对于一个共享变量执行操作是原子的,如果对于多个共享变量操作时,循环 CAS 就无法保证操作的原子性,还是老老实实加锁吧。

    如果本文对你有帮助的话,请留下一个赞吧

  • 相关阅读:
    C++:通过ifstream读取二进制文件内容
    走进苏州的开源创新之旅:开放原子开源大赛苏州站系列活动启幕
    inference.py篇
    音视频播放器开发——实现变速播放
    【列表复制】详解python中list列表复制的几种方法(赋值、切片、copy(),deepcopy())
    Cadence Allegro PCB设计88问解析(二十) 之 Allegro中格点设置(一)
    Android---网络编程优化
    如何解决 chrome 浏览器标签过多无法查看到标题的情况
    conda 解决“libstdc++.so.6: version `GLIBCXX_3.4.20‘ not found“
    冰冰学习笔记:基础IO
  • 原文地址:https://blog.csdn.net/BASK2311/article/details/128198062