• 彻底理解Java并发:Java并发原子类


    本篇内容包括:原子类概述、原子类分类(Atomic 基本类型原子类、Array 数组类型原子类、Atomic\Reference 引用类型原子类、Atomic\FieldUpdater 原子更新属性、Adder 加法器、Accumulator 积累器)、原子类 Demo 等内容!

    一、原子类概述

    我们把一个或者多个操作在 CPU 执行的过程中不能被中断的特性称之为原子性。

    在 Jdk1.5 开始 Java 开始引进提供了 java.util.concurrent.atomic 包,到 Jdk8 时,atomic 包共提供了 16 个原子类,分为 6 种类型,分别是:①、基本类型原子类;②、数组类型原子类;③、引用类型原子类;④、原子更新属性;⑤、Adder 加法器;⑥、积累器。

    当多线程更新变量的值时,可能得不到预期的值,当然增加 syncronized 关键字可以解决线程并发的问题。但原子类提供了一种用法简单,性能高效,线程安全的更新变量的方式。原子类基本都是使用 Unsafe 实现的包装类,主要用到了 Unsafe 的系统层面的 CAS 实现。

    原子类相较于 synchronized 关键字和 lock,有着以下的优点:

    1. 简单:操作简单,底层实现简单

    2. 高效:占用资源少,操作速度快

    3. 安全:在高并发和多线程环境下要保证数据的正确性

    对于是需要简单的递增或者递减的需求场景,使用 synchronized 关键字和 lock 固然可以实现,但代码写的会略显冗余,且性能会有影响,此时用原子类更加方便。


    二、原子类分类

    atomic 包共提供了 16 个原子类,分为 6 种类型:

    1、Atomic(基本类型原子类)

    Atomic 基本类型原子类,包括三种:AtomicInteger、AtomicLong 和 AtomicBoolean。

    AtomicInteger、 AtomicLong、 AtomicBoolean 提供对 int、long、boolean 的原子性操作,这 3 个类提供的方法几乎一模一样。以 AtomicInteger 为例,它包含如下常用的方法:getAndAdd() 返回旧值;addAndGet() 返回新值;getAndIncrement() 加1;incrementAndGet()compareAndSet() 原子替换值等。

    对于其他基本类型的变量,如 char、float、double,可以先转换为整型,然后再进行原子操作。例如,AtomicBoolean 就是先把 Boolean 转换成整型,再使用 compareAndSwaplnt 进行 CAS 操作。

    2、Array(数组类型原子类)

    Array 数组类型原子类,包括三种:AtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray。

    AtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray 提供对 int、long、boolean 的数组元素的原子性操作。原子替换数组中的元素:求i个元素的偏移量,提高位移运算,提高性能。

    3、Atomic\Reference(引用类型原子类)

    Atomic\Reference 引用类型原子类,包括三种:AtomicReference、AtomicStampedReference 和 AtomicMarkableReference。

    AtomicReference 提供了对 对象类型的原子性操作。

    AtomicStampedReference 和 AtomicMarkableReference 以版本戳的方式解决原子类型的 ABA 问题,其中 AtomicStampedReference 是原子更新带有标记位(整数)的引用类型;AtomicMarkableReference 是原子更新带有标记位(布尔)的引用类型。

    4、Atomic\FieldUpdater(原子更新属性)

    Atomic\FieldUpdater 原子更新属性,包括三种:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater。

    Atomic\FieldUpdater 原子更新属性,提供对指定对象的指定字段进行原子性操作

    如果一个类是自己编写的,则可以在编写的时候把成员变量定义为 Atomic 类型。但如果是一个已经有的类,在不能更改其源代码的情况下,要想实现对其成员变量的原子操作,就需要使用 AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater 三个类,将要使用的传给这个类,让其去做原子更新操作。

    5、Adder(加法器

    Adder 加法器,包括两种:LongAdder 和 DoubleAdder。

    Atomic 基本类型,可以保证多线程下的线程安全。但是,在并发量很大的场景下,Atomic 基本类型原子类(AtomicInteger 和 AtomicLong)有很大的性能问题。LongAdder 和 DoubleAdder 就是 Atomic 基本类型原子类的升级类型,专门用于数据统计,性能更高!

    6、Accumulator(积累器)

    Accumulator 积累器,包括两种:LongAccumulator 和 DoubleAccumulator。

    Accumulator 和 Adder 非常相似,实际上 Accumulator 就是一个更通用版本的 Adder,比如 LongAccumulator 是 LongAdder 的功能增强版,因为 LongAdder 的 API 只有对数值的加减,而 LongAccumulator 提供了自定义的函数操作。


    三、原子类Demo

    这里以基本类型原子类中的 AtomicInteger 类为例,介绍通用的 API 接口和使用方法。

    首先是几个常用的API:

    // 以原子方式将给定值与当前值相加,可用于线程中的计数使用,(返回更新的值)。
    int addAndGet(int delta)
    
    // 以原子方式将给定值与当前值相加,可用于线程中的计数使用,(返回以前的值)
    int getAndAdd(int delta)
    
    // 以原子方式将当前值加 1(返回更新的值)
    int incrementAndGet()
    
    // 以原子方式将当前值加 1(返回以前的值)
    int getAndIncrement() 
    
    // 以原子方式设置为给定值(返回旧值)
    int getAndSet(int newValue)
    
    // 以原子方式将当前值减 1(返回更新的值)
    int decrementAndGet()// 以原子方式将当前值减 1(返回以前的值)
    int getAndDecrement()
    
    // 获取当前值
    get()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    这里定义一个临界变量 val,起 10 个异步线程,每个线程都是对这个临界变量进行 10000 次自增操作,如下:

    public class AtomicWrongDemo {
        private int val = 0;
    
        public static void main(String[] args) {
            // 初始化实例
            AtomicWrongDemo atomicWrongDemo = new AtomicWrongDemo();
            for (int i = 0; i < 10; ++i) {
                new Thread(atomicWrongDemo::increase).start();
            }
            // 让主线程休眠3秒,保证前面起的10个异步线程都执行完毕
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicWrongDemo.getVal());
        }
    
        private void increase() {
            for (int i = 0; i < 20000; ++i) {
                ++this.val;
            }
            Thread t = Thread.currentThread();
            System.out.println("线程:" + t.getName() + "已经执行完毕,当前 val 结果为" + this.val);
        }
    
        private int getVal() {
            return this.val;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    运行结果会我们期望的 100000 少很多(操作数越大,距期望值相差越多),比如我这里结果为 39757,出现比 100000 少很多的结果,是因为自增操作 ++i 不是原子操作,出现了竞争,需要对临界变量做同步处理。

    使用 synchronized 关键字和 lock 固然可以实现,但这里只是对临界变量 val++ 时做同步处理,有种高射炮打蚊子的感觉,且加锁后势必会对性能有所印象,这种场景正是我们使用 Atomic 类的场景,如下:

    public class AtomicDemo {
        private AtomicInteger val = new AtomicInteger();
    
        public static void main(String[] args) {
            // 初始化实例
            AtomicDemo atomicDemo = new AtomicDemo();
    
            for (int i = 0; i < 10; ++i) {
                new Thread(atomicDemo::increase).start();
            }
    
            // 让主线程休眠3秒,保证前面起的10个异步线程都执行完毕
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicDemo.getVal().toString());
        }
    
        private void increase() {
            for (int i = 0; i < 10000; ++i) {
                this.val.incrementAndGet();
            }
        }
    
        private AtomicInteger getVal() {
            return this.val;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    这里我们使用了 AtomicInterger 类的 increamentAndGet 方法,以原子方式将当前值加 1(返回更新的值),结果自然是每次运行都打印 100000,可以看到代码写起来很简洁,很轻量级。

  • 相关阅读:
    golang的\t的使用方法
    蓝桥杯 题库 简单 每日十题 day6
    element-plus form表单的二次封装
    [JS] DOM 的事件监听、混杂模式和标准模式适配
    2022世界杯结果预测,简单AI模型最有效?附代码!
    2023研究生数学建模E题保姆级思路 出血性脑卒中临床智能诊疗
    CenOS 7 网络连接设置
    【Python数据科学快速入门系列 | 02】创建ndarray对象的十多种方法
    SpringBoot_05_自动配置管理
    神经网络 炒股,神经网络 软件
  • 原文地址:https://blog.csdn.net/weixin_45187434/article/details/127618658