乐趣区

一文读懂-Java-中的原子类

一、无锁方案

Java 并发包中的原子类都是基于无锁方案实现的,相较于传统的互斥锁,无锁并没有加锁、解锁、线程切换的消耗,因此无锁解决方案的性能更好,同时无锁还能够保证线程安全。

1. 无锁方案的实现原理

无锁主要依赖 CAS(Compare And Swap),即比较并交换,CAS 是一条 CPU 指令,其本身是能够保证原子性的。CAS 中有三个参数:

  • 共享变量的内存地址 A
  • 用于比较的值 B
  • 共享变量的新值 C
public class SimpleCAS {

    private int value;

    public synchronized int cas(int expectVal, int newVal){
        int curVal = value;
        if (expectVal == curVal){value = newVal;}
        return curVal;
    }
}

上面的代码展示了 CAS 的简单实现,从内存中读出当前 value 的值,并且需要判断,期望值 expectVal == curVal 的时候,才会将 value 更新为新值。

仍然以上面的代码,来实现一个简单的,基于 CAS 的线程安全的 value+1 方法。这里的 cas 方法仅用于帮助理解,所以执行结果可能有出入。

public class SimpleCAS {

    private volatile int value;

    public void addValue(){
        int newVal = value + 1;
        while (value != cas(value, newVal)){newVal = value + 1;}
    }
    
    private synchronized int cas(int expectVal, int newVal){
        int curVal = value;
        if (expectVal == curVal){value = newVal;}
        return curVal;
    }
}

线程首先读取 value 的值并加 1,如果此时有另一个线程更新了 value,则期望值和 value 不相等,更新失败。更新失败后,循环尝试,重新读取 value 的值,直到更新成功退出循环。

2. ABA 问题

无锁的实现方案中需要注意的一个问题便是 ABA 问题。

例如上面的代码,value 的初始值为 0,线程 t1 取到了 value 的值,并将其更新为 1,然后线程又将 value 更新为 0。

假如这个过程中有另外一个线程 t2,和 t1 同时取初始值为 0 的 value,t2 在 t1 执行完后更新 value,这个时候 value 虽然还是为 0,但已经被 t1 修改过了。

大多数情况下,我们并不需要关心 ABA 问题,例如数值型数据的加减,但是对象类型的数据遇到了 ABA 问题的话,可能前后的属性已经发生了变化,所以需要解决。

解决的办法也很简单,给对象类型的数据加上一个版本号即可,每更新一次,版本号加 1,这样即使对象数据从 A 变成 B 后 又变成 A,但是版本号是递增的,就可以分辨出对象还是被修改过的。

二、原子类

1. 原子化基本数据类型

有三个实现类:AtomicBoolean、AtomicInteger、AtomicLong

常用的方法如下,以 AtomicInteger 为例,其他的类似:

AtomicInteger i = new AtomicInteger(0);

i.getAndSet(int newValue);// 获取当前值并设置新值

i.getAndIncrement();// 相当于 i ++
i.incrementAndGet();// 相当于 ++ i

i.getAndDecrement();// 相当于 i --
i.decrementAndGet();// 相当于 -- i

i.addAndGet(int delta);// 相当于 i + delta,并返回添加后的值
i.getAndAdd(int delta);// 相当于 i + delta,并返回添加前的值

i.compareAndSet(int expect, int update);//CAS 操作,返回 boolean 值,表示是否更新成功

i.getAndUpdate(update -> 10);// 通过函数更新值
i.updateAndGet(update -> 10);// 类似上面 

2. 原子化对象引用类型

实现类分别是:AtomicReference、AtomicStampedReference、AtomicMarkableReference,其中后两个可以实现了解决 ABA 问题的方案。

AtomicReference 常用的方法如下:

// 假设有一个叫做 Order 的类
AtomicReference<Order> orderReference = new AtomicReference<>();

orderReference.getAndSet(Order newValue);// 获取并设置

orderReference.set(Order order);// 设置值
Order order1 = orderReference.get();// 获取对象

orderReference.compareAndSet(Order expect, Order update);// 比较交换

orderReference.getAndUpdate();// 通过函数更新值
orderReference.updateAndGet();

AtomicStampedReference 需要传入初始值和初始 stamp,其中 stamp 相当于对象的版本号(用来解决 ABA 问题),使用示例如下:

AtomicStampedReference<String> reference = new AtomicStampedReference<>("roseduan", 0);

// 尝试修改 stamp 的值
boolean b = reference.attemptStamp(reference.getReference(), 10);

// 获取值
String str = reference.getReference();

// 获取 stamp
int stamp = reference.getStamp();

// 重新设置值和 stamp
reference.set("I am not roseduan", 20);

// 比较交换
boolean b1 = reference.compareAndSet("roseduan", "jack", 20, 0);

AtomicMarkableReference 使用一个 mark 标记 (boolean 类型) 代替了 AtomicStampedReference 中的 stamp,用这种更简单的方式来解决 ABA 问题。使用的方式和上面的类似,只是将方法中的 stamp 变为 boolean 类型的值即可。

3. 原子化数组类型

实现类有三个:

  • AtomicIntegerArray:原子化的整型数组
  • AtomicLongArray:原子化长整型数组
  • AtomicReferenceArray:原子化对象引用数组

使用和原子化基本类型都是差不多的,只是需要在方法中加上数组下标即可。

4. 原子化对象属性更新器

也有三个实现类:

  • AtomicIntegerFieldUpdater:更新对象的整型属性
  • AtomicLongFieldUpdater:更新对象的长整型属性
  • AtomicReferenceFieldUpdater:更新对象的引用型属性

这三个类都是利用 Java 的反射机制实现的,并且为了保证原子性,要求被更新的对象的属性必须是 volatile 类型的。使用示例如下:

@Data
@Builder
public class User {

    private volatile int age;

    private volatile long number;

    private volatile String name;

    public static void main(String[] args) {User user = User.builder().age(22).number(15553663L).name("roseduan").build();

        // 更新 age 属性的值
        AtomicIntegerFieldUpdater<User> integerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
        integerFieldUpdater.set(user, 25);

        // 更新 number 属性的值
        AtomicLongFieldUpdater<User> longFieldUpdater = AtomicLongFieldUpdater.newUpdater(User.class, "number");
        longFieldUpdater.set(user, 1000101L);

        // 更新对象类型的属性的值
        AtomicReferenceFieldUpdater<User, String> referenceFieldUpdater = AtomicReferenceFieldUpdater.newUpdater(User.class, String.class, "name");
        referenceFieldUpdater.set(user, "I am not roseduan");

        System.out.println(user.toString());
    }
}

程序中创建了一个 User 类,有三个属性 age、number、name 分别对应整型、长整型、引用类型。然后使用对象属性更新器进行属性值的更新,更新器的其他方法的使用和前面说到的几种原子化类型类似。

5. 原子化累加器

实现类有四个:

  • DoubleAdder
  • DoubleAccumulator
  • LongAdder
  • LongAccumulator

这几个类的功能有限,仅用来执行累加操作,但是速度非常快。下面介绍 DoubleAdder 和 DoubleAccumulator 的用法,其余两个类似。

//DoubleAccumulator 使用示例
DoubleAccumulator a = new DoubleAccumulator(Double::sum, 0);// 设初始值为 0
// 累加
a.accumulate(1);
a.accumulate(2);
a.accumulate(3);
a.accumulate(4);

System.out.println(a.get());// 输出 10

//DoubleAdder 使用示例
DoubleAdder adder = new DoubleAdder();
adder.add(1);
adder.add(2);
adder.add(3);
adder.add(4);
adder.add(5);

System.out.println(adder.intValue());// 输出 15
退出移动版