当前位置:首页 > 技术分析 > 正文内容

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

ruisui883个月前 (03-11)技术分析13

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 此时不能修改,并重新获取现在的最新值,重新获取的动作就是自旋
public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
 }
复制代码
  • valueOffset:偏移量表示该变量值相对于当前对象地址的偏移,Unsafe 就是根据内存偏移地址获取数据

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

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

原子引用

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

  1. AtomicReference类

普通的原子类对象

public class AtomicReferenceDemo {
    public static void main(String[] args) {
        User user1 = new User("旭阳");

        // 创建原子引用包装类
        AtomicReference atomicReference = new AtomicReference<>(user1);

        while (true) {
            User user2 = new User("alvin");
            // 比较并交换
            if (atomicReference.compareAndSet(user1, user2)) {
                break;
            }
        }
        System.out.println(atomicReference.get());
    }
}

@Data
@AllArgsConstructor
@ToString
class User {
    private String name;
}
复制代码
  • 调用compareAndSet()方法进行比较替换对象

ABA问题

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

  1. AtomicStampedReference类

带版本号的原子类对象

@Slf4j(topic = "a.AtomicStampedReferenceTest")
public class AtomicStampedReferenceTest {
    // 构造AtomicStampedReference
    static AtomicStampedReference ref = new AtomicStampedReference<>("A", 0);
    
    public static void main(String[] args) throws InterruptedException {
        log.debug("main start...");
        // 获取值 A
        String prev = ref.getReference();
        // 获取版本号
        int stamp = ref.getStamp();
        log.debug("版本 {}", stamp);
        // 如果中间有其它线程干扰,发生了 ABA 现象
        other();
        Thread.sleep(1000);
        // 尝试改为 C
        log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));
    }
    private static void other() throws InterruptedException {
        new Thread(() -> {
            log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B",
                    ref.getStamp(), ref.getStamp() + 1));
            log.debug("更新版本为 {}", ref.getStamp());
        }, "t1").start();

        Thread.sleep(500);

        new Thread(() -> {
            log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A",
                    ref.getStamp(), ref.getStamp() + 1));
            log.debug("更新版本为 {}", ref.getStamp());
        }, "t2").start();
    }
}
复制代码

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

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

@Slf4j(topic = "c.AtomicMarkableReferenceTest")
public class AtomicMarkableReferenceTest {
    // 构造 AtomicMarkableReference, 初始标记为false
    static AtomicMarkableReference ref = new AtomicMarkableReference<>("A", false);

    public static void main(String[] args) throws InterruptedException {
        log.debug("main start...");
        other();
        Thread.sleep(1000);
        // 看看是否发生了变化
        log.debug("change {}", ref.isMarked());
    }
    private static void other() throws InterruptedException {
        new Thread(() -> {
            log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B",
                    false, true));
        }, "t1").start();

        Thread.sleep(500);

        new Thread(() -> {
            log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A",
                    true, true));
        }, "t2").start();
    }
}
复制代码

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

原子数组

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

直接上例子

public class AtomicIntegerArrayTest {
    public static void main(String[] args) throws Exception{
        AtomicIntegerArray array = new AtomicIntegerArray(10);
        Thread t1 = new Thread(()->{
            int index;
            for(int i=1; i<100000; i index='i%10;' 09 array.incrementandgetindex thread t2='new' thread->{
            int index;
            for(int i=1; i<100000; i++) {
                index = i%10; //范围0~9
                array.decrementAndGet(index);
            }
        });
        t1.start();
        t2.start();
        Thread.sleep(5 * 1000);
        System.out.println(array.toString());
    }
}
复制代码

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

原子字段更新器

  • AtomicReferenceFieldUpdater
  • AtomicIntegerFieldUpdater
  • AtomicLongFieldUpdater

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

@Data
public class AtomicReferenceFieldUpdaterTest {

    private volatile int age = 10;

    private int age2;

    public static void main(String[] args) {
        AtomicIntegerFieldUpdater integerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(AtomicReferenceFieldUpdaterTest.class, "age");

        AtomicReferenceFieldUpdaterTest ref = new AtomicReferenceFieldUpdaterTest();
        // 对volatile 的age字段+1
        integerFieldUpdater.getAndIncrement(ref);
        System.out.println(ref.getAge());

        // 修改 非volatile的age2
        integerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(AtomicReferenceFieldUpdaterTest.class, "age2");
        integerFieldUpdater.getAndIncrement(ref);
    }
}
复制代码

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

原子累加器

原子累加器主要是用来做累加的,相关的类有LongAdder、DoubleAdder、LongAccumulator、DoubleAccumulator。

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

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

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

public class LongAdderTest {

    public static void main(String[] args) {
        System.out.println("LongAdder ...........");
        for (int i = 0; i < 5 i addfunc -> new LongAdder(), adder -> adder.increment());
        }
        System.out.println("AtomicLong ...........");
        for (int i = 0; i < 5 i addfunc -> new AtomicLong(), adder -> adder.getAndIncrement());
        }
    }

    private static  void addFunc(Supplier adderSupplier, Consumer action) {
        T adder = adderSupplier.get();
        long start = System.nanoTime();
        List ts = new ArrayList<>();

        // 40个线程,每人累加 50 万
        for (int i = 0; i < 40 i ts.addnew thread -> {
                for (int j = 0; j < 500000 j action.acceptadder ts.foreacht -> t.start());
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.nanoTime();
        System.out.println(adder + " cost:" + (end - start)/1000_000);
    }
}
复制代码

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

总结

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


更多技术干货和学习资料关注公众号——JAVA旭阳

扫描二维码推送至手机访问。

版权声明:本文由ruisui88发布,如需转载请注明出处。

本文链接:http://www.ruisui88.com/post/2692.html

标签: ts void
分享给朋友:

“盘点JAVA中基于CAS实现的原子类” 的相关文章

2021最全VUE面试题,奥利给

1. vue-router用过没,哪些常用的钩子函数路由钩子的执行流程,钩子函数种类有:全局守卫、路由守卫、组件守卫。首页可以控制导航跳转, beforeEach , afterEach 等,?般?于页? title 的修改。?些需要登录才能调整??的重定向功能。beforeEach 主要有3个参数...

抖音 Android 性能优化系列:启动优化实践

启动性能是 APP 使用体验的门面,启动过程耗时较长很可能使用户削减使用 APP 的兴趣,抖音通过对启动性能做劣化实验也验证了其对于业务指标有显著影响。抖音有数亿的日活,启动耗时几百毫秒的增长就可能带来成千上万用户的留存缩减,因此,启动性能的优化成为了抖音 Android 基础技术团队在体验优化方向...

摄影后期必看 | PS插件camera raw 16.4教程 | 范围蒙版

范围蒙版Camera Raw 【蒙版】模块中提供了三个范围蒙版工具,可以通过特定的范围来创建蒙版。此次新增的【范围蒙版】大大加强了acr插件对局部调整的能力。点击下拉小箭头可以看到【颜色范围】,可用于快速选择想要编辑的颜色。快捷键:Shift + C【明亮度范围】,可用于快速选择想要调整的明亮度。快...

Excel中的FILTER函数详细介绍及使用示例

在Excel中处理大量数据时,经常需要根据特定条件筛选出符合条件的数据行或列。这正是Excel的FILTER函数发挥作用的地方。FILTER函数是Excel中一个非常强大的工具,它可以基于一个或多个条件动态地过滤数据,使数据分析和报告制作变得更加高效和准确。本文将详细介绍FILTER函数的用法,并提...

Python中的11 种数组算法

1. 创建数组 创建数组意味着留出一个连续的内存块来存储相同类型的元素。在大多数语言中,您可以在创建数组时指定数组的大小。假设您正在书架上整理一组书籍,并且您需要为正好 10 本书预留空间。功能架上的每个空间都对应于数组中的一个索引。# Example in Python arr = [1, 2,...

一套代码,多端运行——使用Vue3开发兼容多平台的小程序

介绍Vue3发布已经有一段时间了,从目前来看,其生态还算可以,也已经有了各种组件库给予了支持,但是不管是Vue3还是Vue2都无法直接用来开发小程序,因此国内一些技术团队针对Vue开发了一些多端兼容运行的开发框架,今天来体验一下使用Taro来体验一下使用Vue3开发多平台运行的小程序,以便于兼容各大...