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

Java
364
0
0
2022-12-20
标签   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()

这里定义一个临界变量 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;
    }
}

运行结果会我们期望的 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;
    }
}

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