当前位置:

首页 > 编程开发 > Java的原子性Atomic如何使用

Java的原子性Atomic如何使用

线程安全当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协调,这个类都能表现出正确的行为,那么就称这个类时线程安全的。线程安全主要体现在以下三个方面原子性:提供了互斥访问,同一时刻只能有一个线程对它进行操作可见性:一个线程对主内存的修改可以及时的被其他线程观察到有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序JUC中的Atomic包详解Atomic包中提供了很多Atomicxxx的类:它们都是

本文目录

    线程安全

    当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协调,这个类都能表现出正确的行为,那么就称这个类时线程安全的。

    线程安全主要体现在以下三个方面

    • 原子性:提供了互斥访问,同一时刻只能有一个线程对它进行操作

    • 可见性:一个线程对主内存的修改可以及时的被其他线程观察到

    • 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序

    JUC中的Atomic包详解

    Atomic包中提供了很多Atomicxxx的类:

    Java的原子性Atomic如何使用

    它们都是CAS(compareAndSwap)来实现原子性。

    先写一个简单示例如下:

    @Slf4j
    public class AtomicExample1 { 
        // 请求总数
        public static int clientTotal = 5000; 
        // 同时并发执行的线程数
        public static int threadTotal = 200; 
        public static AtomicInteger count = new AtomicInteger(0); 
        public static void main(String[] args) throws Exception {
    
            ExecutorService executorService = Executors.newCachedThreadPool();
            final Semaphore semaphore = new Semaphore(threadTotal);
            final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
            for (int i = 0; i < clientTotal ; i++) {
                executorService.execute(() -> {
                    try {
                        semaphore.acquire();
                        add();
                        semaphore.release();
                    } catch (Exception e) {
                        log.error("exception", e);
                    }
                    countDownLatch.countDown();
                });
            }
            countDownLatch.await();
            executorService.shutdown();
            log.info("count:{}", count.get());
        }
     
        private static void add() {
            count.incrementAndGet();
        }
    }

    可以发下每次的运行结果总是我们想要的预期结果5000。说明该计数方法是线程安全的。

    我们查看下count.incrementAndGet()方法,它的第一个参数为对象本身,第二个参数为valueOffset是用来记录value本身在内存的编译地址的,这个记录,也主要是为了在更新操作在内存中找到value的位置,方便比较,第三个参数为常量1

    public class AtomicInteger extends Number implements java.io.Serializable {
        private static final long serialVersionUID = 6214790243416807050L;
     
        // setup to use Unsafe.compareAndSwapInt for updates
        private static final Unsafe unsafe = Unsafe.getUnsafe();
        private static final long valueOffset;
     
        static {
            try {
                valueOffset = unsafe.objectFieldOffset
                    (AtomicInteger.class.getDeclaredField("value"));
            } catch (Exception ex) { throw new Error(ex); }
        }
     
        private volatile int value; 
     
        ... 此处省略多个方法...
     
        /**
         * Atomically increments by one the current value.
         *
         * @return the updated value
         */
        public final int incrementAndGet() {
            return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
        }
    }

    AtomicInteger源码里使用了一个Unsafe的类,它提供了一个getAndAddInt的方法,我们继续点看查看它的源码:

    public final class Unsafe {
        private static final Unsafe theUnsafe;
     
        ....此处省略很多方法及成员变量.... 
     
     public final int getAndAddInt(Object var1, long var2, int var4) {
            int var5;
            do {
                var5 = this.getIntVolatile(var1, var2);
            } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); 
            return var5;
        } 
     
     public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); 
     public native int getIntVolatile(Object var1, long var2);
    }

    可以看到这里使用了一个do while语句来做主体实现的。而在while语句里它的核心是调用了一个compareAndSwapInt()的方法。它是一个native方法,它是一个底层的方法,不是使用Java来实现的。

    假设我们要执行0+1=0的操作,下面是单线程情况下各参数的值:

    Java的原子性Atomic如何使用

    Java的原子性Atomic如何使用

    更新后:

    Java的原子性Atomic如何使用

    compareAndSwapInt()方法的第一个参数(var1)是当前的对象,就是代码示例中的count。此时它的值为0(期望值)。第二个值(var2)是传递的valueOffset值,它的值为12。第三个参数(var4)就为常量1。方法中的变量参数(var5)是根据参数一和参数二valueOffset,调用底层getIntVolatile方法得到的值,此时它的值为0 。compareAndSwapInt()想要达到的目标是对于count这个对象,如果当前的期望值var1里的value跟底层的返回的值(var5)相同的话,那么把它更新成var5+var4这个值。不同的话重新循环取期望值(var5)直至当前值与期望值相同才做更新。compareAndSwap方法的核心也就是我们通常所说的CAS。

    Atomic包下其他的类如AtomicLong等的实现原理基本与上述一样。

    这里再介绍下LongAdder这个类,通过上述的分析,我们已经知道了AtomicLong使用CAS:在一个死循环内不断尝试修改目标值直到修改成功。如果在竞争不激烈的情况下,它修改成功概率很高。反之,如果在竞争激烈的情况下,修改失败的概率会很高,它就会进行多次的循环尝试,因此性能会受到一些影响。

    对于普通类型的long和double变量,jvm允许将64位的读操作或写操作拆成两个32位的操作。LongAdder的核心思想是将热点数据分离,它可以将AtomicLong内部核心数据value分离成一个数组,每个线程访问时通过hash等算法映射到其中一个数字进行计数。而最终的计数结果则为这个数组的求和累加,其中热点数据value,它会被分离成多个单元的cell,每个cell独自维护内部的值,当前对象的实际值由所有的cell累计合成。这样,热点就进行了有效的分离,提高了并行度。LongAdder相当于在AtomicLong的基础上将单点的更新压力分散到各个节点上,在低并发的时候对base的直接更新可以很好的保障跟Atomic的性能基本一致。而在高并发的时候,通过分散提高了性能。但是如果在统计的时候有并发更新,可能会导致统计的数据有误差。

    在实际高并发计数的时候,可以优先使用LongAdder。在低并行度或者需要准确数值的时候可以优先使用AtomicLong,这样反而效率更高。

    下面简单的演示下Atomic包下AtomicReference简单的用法:

    @Slf4j
    public class AtomicExample4 { 
        private static AtomicReference count = new AtomicReference<>(0); 
        public static void main(String[] args) {
            count.compareAndSet(0, 2); 
            count.compareAndSet(0, 1); 
            log.info("count:{}", count.get());
        }
    }

    compareAndSet()分别传入的是预期值跟更新值,只有当预期值跟当前值相等时,才会将值更新为更新值;

    上面的第一个方法可以将值更新为2,而第二个步中无法将值更新为1。

    下面简单介绍下AtomicIntegerFieldUpdater 用法(利用原子性去更新某个类的实例):

    @Slf4j
    public class AtomicExample5 { 
        private static AtomicIntegerFieldUpdater updater =
                AtomicIntegerFieldUpdater.newUpdater(AtomicExample5.class, "count");
     
        @Getter
        private volatile int count = 100; 
        public static void main(String[] args) { 
            AtomicExample5 example5 = new AtomicExample5();
     
            if (updater.compareAndSet(example5, 100, 120)) {
                log.info("update success 1, {}", example5.getCount());
            }
     
            if (updater.compareAndSet(example5, 100, 120)) {
                log.info("update success 2, {}", example5.getCount());
            } else {
                log.info("update failed, {}", example5.getCount());
            }
        }
    }

    它可以更新某个类中指定成员变量的值。

    注意:修改的成员变量需要用volatile关键字来修饰,并且不能是static描述的字段。

    AtomicStampReference这个类它的核心是要解决CAS的ABA问题(CAS操作的时候,其他线程将变量的值A改成了B,接着又改回了A,等线程使用期望值A与当前变量进行比较的时候,发现A变量没有变,于是CAS就将A值进行了交换操作。

    实际上该值已经被其他线程改变过)。

    ABA问题的解决思路就是每次变量变更的时候,就将版本号加一。

    看一下它的一个核心方法compareAndSet():

    public class AtomicStampedReference { 
        private static class Pair {
            final T reference;
            final int stamp;
            private Pair(T reference, int stamp) {
                this.reference = reference;
                this.stamp = stamp;
            }
            static  Pair of(T reference, int stamp) {
                return new Pair(reference, stamp);
            }
        }
     
       ... 此处省略多个方法 ....
     
       public boolean compareAndSet(V   expectedReference,
                                     V   newReference,
                                     int expectedStamp,
                                     int newStamp) {
            Pair current = pair;
            return
                expectedReference == current.reference &&
                expectedStamp == current.stamp &&
                ((newReference == current.reference &&
                  newStamp == current.stamp) ||
                 casPair(current, Pair.of(newReference, newStamp)));
        }
    }

    可以看到它多了一个stamp的比较,stamp的值是由每次更新的时候进行维护的。

    再介绍下AtomicLongArray,它维护了一个数组。在该数组下,我们可以选择性的已原子性操作更新某个索引对应的值。

    public class AtomicLongArray implements java.io.Serializable {
        private static final long serialVersionUID = -2308431214976778248L;
     
        private static final Unsafe unsafe = Unsafe.getUnsafe();
     
        ...此处省略....
     
     
        /**
         * Atomically sets the element at position {@code i} to the given value
         * and returns the old value.
         *
         * @param i the index
         * @param newValue the new value
         * @return the previous value
         */
        public final long getAndSet(int i, long newValue) {
            return unsafe.getAndSetLong(array, checkedByteOffset(i), newValue);
        }
     
        /**
         * Atomically sets the element at position {@code i} to the given
         * updated value if the current value {@code ==} the expected value.
         *
         * @param i the index
         * @param expect the expected value
         * @param update the new value
         * @return {@code true} if successful. False return indicates that
         * the actual value was not equal to the expected value.
         */
        public final boolean compareAndSet(int i, long expect, long update) {
            return compareAndSetRaw(checkedByteOffset(i), expect, update);
        }
    }

    最后再写一个AtomcBoolean的简单使用:

    @Slf4j
    public class AtomicExample6 { 
        private static AtomicBoolean isHappened = new AtomicBoolean(false);
     
        // 请求总数
        public static int clientTotal = 5000;
     
        // 同时并发执行的线程数
        public static int threadTotal = 200;
     
        public static void main(String[] args) throws Exception {
            ExecutorService executorService = Executors.newCachedThreadPool();
            final Semaphore semaphore = new Semaphore(threadTotal);
            final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
            for (int i = 0; i < clientTotal ; i++) {
                executorService.execute(() -> {
                    try {
                        semaphore.acquire();
                        test();
                        semaphore.release();
                    } catch (Exception e) {
                        log.error("exception", e);
                    }
                    countDownLatch.countDown();
                });
            }
            countDownLatch.await();
            executorService.shutdown();
            log.info("isHappened:{}", isHappened.get());
        }
     
        private static void test() {
            if (isHappened.compareAndSet(false, true)) {
                log.info("execute");
            }
        }
    }
    本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
    作者最新文章
    编程开发 Java
    相关文章 更多
    Java单元测试入门:使用JUnit 5编写第一个测试用例
    Java单元测试入门:使用JUnit 5编写第一个测试用例

    面向初学者的JUnit 5实用入门指南。从测试概念、Maven依赖配置,到手把手编写首个单元测试用例及断言验证,助你掌握Java单元测试的核心流程。

    Java变量未初始化报错解析:局部变量与成员变量默认值差异
    Java变量未初始化报错解析:局部变量与成员变量默认值差异

    深入解析Java中局部变量未初始化编译报错与成员变量自动赋予默认值的机制差异,涵盖JVM内存分配、编译期确定性赋值检查与实际排查修复方案。

    Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat
    Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat

    详细讲解在Java中获取当前系统时间并进行格式化输出的两种核心方案:基于Java 8全新日期时间API的DateTimeFormatter与传统遗留的SimpleDateFormat,包含完整代码示例、线程安全机制对比及生产选型建议。

    Java中final与static final常量的定义及初始化规则
    Java中final与static final常量的定义及初始化规则

    深入解析Java中final变量与static final常量的定义、内存语义、初始化时机及合法赋值路径,涵盖编译期常量优化与实战避坑指南。

    Java抽象方法与接口方法的区别及选型建议
    Java抽象方法与接口方法的区别及选型建议

    深入对比Java中抽象类抽象方法与接口方法的语法演进、设计意图及运行期差异,并提供清晰的架构选型决策流程与实战范例。

    Java实现HTTP请求与JSON解析的完整代码示例
    Java实现HTTP请求与JSON解析的完整代码示例

    本文提供Java标准库HttpClient发起HTTP GET/POST请求并结合Jackson进行JSON数据反序列化与序列化的完整代码示例与工程实践指南。

    Java递归出现StackOverflowError的原因分析及栈空间优化方案
    Java递归出现StackOverflowError的原因分析及栈空间优化方案

    详细分析Java递归调用出现StackOverflowError的原因,结合代码说明线程栈、栈帧和递归深度之间的关系,并介绍终止条件排查、迭代改写、减少栈帧开销以及通过-Xss调整JVM栈空间的方法。

    Java空指针异常(NPE)的代码排查与修复实例
    Java空指针异常(NPE)的代码排查与修复实例

    针对Java中常见的NullPointerException(NPE),本文从堆栈日志定位、典型代码触发场景、具体修复代码对比到防御体系进行系统讲解,提供实用且易上手的排查与修复方案。

    selenium上传文件教程本地文件路径设置方法
    selenium上传文件教程本地文件路径设置方法

    深入解析Selenium处理文件上传的技术细节,涵盖标准HTML控件的路径直接注入法与非标准控件的系统级模拟方案,提供Windows与Linux环境下的路径设置最佳实践及常见异常排查。

    大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景
    大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景

    针对大数据工程场景,系统对比Java、Scala与Python在执行性能、生态结合、开发效率和维护成本上的核心差异,给出清晰的架构选型依据与落地方案。

    查看更多
    精品专题 更多
    装机必备
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows
    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    PDF教程
    PDF教程

    正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

    Mac软件 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    Aperty macOS版
    Aperty macOS版
    macOS

    Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

    WINDOWS 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    PixTV 网页版
    PixTV 网页版
    Windows

    PixTV 是一款在浏览器中使用的 AI 视觉内容创作平台,提供图片、视频、音频、文字生成、AI 分镜、局部重绘、提示词反推和无限画布等工具,适合短剧、漫剧、广告及社交媒体内容制作。