JDK 原子类工具 (java.util.concurrent.atomic)

博文目录


CAS+自旋 无锁原子效果 线程安全

JDK Unsafe CAS+自旋 实现无锁原子操作

原子工具类

[Java并发与多线程](十八)atomic包

JDK 并发包下的原子工具类, 支持对单个变量进行无锁线程安全编程, 该变量可以是 volatile 修饰的 值, 字段, 数组元素等
在这里插入图片描述
特点:不可分割,一个操作是不可中断的,即便是多线程的情况下也可以保证。
作用:原子类的作用和锁类似,是为了保证并发情况下的线程安全。

相较于锁的优势

  • 粒度更细:原子操作可以把竞争范围缩小到变量级别,这是我们可以获得的最细粒度的情况了,通常锁的粒度都要大于原子变量的粒度;
  • 效率更高:通常,使用原子类的效率会比使用锁的效率更高,除了高度竞争的情况。

原子工具类分类

分类工具
基本类型原子类AtomicInteger, AtomicLong, AtomicBoolean
数组类型原子类AtomicIIntegerArray, AtomicLongArray, AtomicReferenceArray
引用类型原子类AtomicReference, AtomicStampedReference, AtomicMarkableReference
字段升级原子类AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater
adder 累加器LongAdder, DoubleAdder
accumulator 累加器LongAccumulator, DoubleAccumulator
累加器的底层支持, 支持 64 位值的动态条带化Striped64

基本类型原子类

针对 volatile 修饰的单个 int 或 long 类型的变量提供无锁线程安全的方法

AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference 这些原子类型,它们无一例外都采用了基于 volatile 关键字 +CAS 算法无锁的操作方式来确保共享数据在多线程操作下的线程安全性。

  • volatile关键字保证了线程间的可见性,当某线程操作了被volatile关键字修饰的变量,其他线程可以立即看到该共享变量的变化。
  • CAS算法,即对比交换算法,是由UNSAFE提供的,实质上是通过操作CPU指令来得到保证的。CAS算法提供了一种快速失败的方式,当某线程修改已经被改变的数据时会快速失败。
  • 当CAS算法对共享数据操作失败时,因为有自旋算法的加持,我们对共享数据的更新终究会得到计算。

AtomicInteger

内部维护了一个 volatile 的 int 属性, 提供了 getAndSet, getAndIncrement, getAdnDecrement, incrementAndGet, decrementAndGet, getAndAdd, addAndGet 等使用CAS自旋保证原子操作效果的线程安全的实用方法

package java.util.concurrent.atomic;
import java.util.function.IntUnaryOperator;
import java.util.function.IntBinaryOperator;
import sun.misc.Unsafe;

public class AtomicInteger extends Number implements java.io.Serializable {

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    // AtomicInteger对象持有一个int类型的value
    // valueOffset就是value属性的内存地址相对于AtomicInteger内存地址的偏移量
    // 即AtomicInteger的内存地址, 加上该偏移, 就是AtomicInteger对象中value属性的内存地址
    private static final long valueOffset;

    static {
        try {
        	// 获取value相对于AtomicInteger对象的偏移
            valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

	// volatile, 保证内存可见性, 即任何一个线程修改了value, 都将强制刷新到主存, 并通知其他线程更新最新的value
    private volatile int value;

	// 立即set, 修改volatile变量, 内存可见
    public final void set(int newValue) {
        value = newValue;
    }

	// 延迟set, 即通过非volatile的方式修改变量, 不强制内存可见
    public final void lazySet(int newValue) {
        unsafe.putOrderedInt(this, valueOffset, newValue);
    }

	// 获取旧值, 并设置新值
    public final int getAndSet(int newValue) {
        return unsafe.getAndSetInt(this, valueOffset, newValue);
    }

	// 比较旧值和期望值, 相同则更新新值, 就是一个CAS操作
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

	// 比较旧值和期望值, 相同则更新新值, 就是一个CAS操作
	// 在8里面weakCompareAndSet和compareAndSet完全相同
	// 在11里加了 `@Deprecated(since="9")` 弃用注解, 用 weakCompareAndSetPlain 来代替
	// 后续的版本里有 @HotSpotIntrinsicCandidate 注解, 貌似是有一定的编译器优化
	// https://blog.csdn.net/superfjj/article/details/107680892 JVM详解之:HotSpot VM中的Intrinsic methods
	// 8的注释引用内容翻译: 原子类也支持方法weakCompareAndSet,它的适用性有限。在某些平台上,弱版本在正常情况下可能比 compareAndSet 更有效,但不同之处在于,任何给定的 weakCompareAndSet 方法调用都可能虚假地返回 false(也就是说,没有明显的原因)。错误返回仅意味着可以在需要时重试操作,这依赖于当变量保持预期值并且没有其他线程也尝试设置变量时重复调用最终会成功的保证。 (例如,这种虚假故障可能是由于与预期值和当前值是否相等无关的内存争用效应造成的。)此外,weakCompareAndSet 不提供同步控制通常需要的排序保证。然而,当这些更新与程序的其他发生前发生的顺序无关时,该方法对于更新计数器和统计数据可能是有用的。当线程看到由weakCompareAndSet 引起的原子变量更新时,它不一定看到在weakCompareAndSet 之前发生的任何其他变量的更新。例如,在更新性能统计信息时,这可能是可以接受的,但很少会这样。
    public final boolean weakCompareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

 	// 获取旧值, 并设置新值, 新值是旧值加1
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

	// 获取旧值, 并设置新值, 新值是旧值-1
    public final int getAndDecrement() {
        return unsafe.getAndAddInt(this, valueOffset, -1);
    }

	// 获取旧值, 并设置新值, 新值是旧值+delta
    public final int getAndAdd(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }

	// 设置新值, 新值是旧值+1, 返回新值
	// unsafe.getAndAddInt(this, valueOffset, 1), 给旧值+1, 但返回的是旧值
	// 但这里要求自增后在获取值, 所以后面又加了个1
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

    public final int decrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
    }

    public final int addAndGet(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
    }

    // 获取旧值, 并设置新值, 传入的是一个一元IntOperator, 即输入int输出int的Function
    public final int getAndUpdate(IntUnaryOperator updateFunction) {
        int prev, next;
        do {
        	// 获取旧值
            prev = get();
            // 传入旧值, 应用传入的Function来得到新值
            next = updateFunction.applyAsInt(prev);
            // CAS更新新值
        } while (!compareAndSet(prev, next));
        return prev;
    }

	// 设置新值, 然后返回新值
    public final int updateAndGet(IntUnaryOperator updateFunction) {
        int prev, next;
        do {
            prev = get();
            next = updateFunction.applyAsInt(prev);
        } while (!compareAndSet(prev, next));
        return next;
    }

	// 获取旧值, 并设置新值, 传入的是一个二元的IntOperator, 即输入两个int输出int的Function
    public final int getAndAccumulate(int x, IntBinaryOperator accumulatorFunction) {
        int prev, next;
        do {
            prev = get();
            next = accumulatorFunction.applyAsInt(prev, x);
        } while (!compareAndSet(prev, next));
        return prev;
    }

    public final int accumulateAndGet(int x, IntBinaryOperator accumulatorFunction) {
        int prev, next;
        do {
            prev = get();
            next = accumulatorFunction.applyAsInt(prev, x);
        } while (!compareAndSet(prev, next));
        return next;
    }

}

AtomicLong

和 AtomicInteger 一样, 就是 int value 变成了 long value

AtomicBoolean

和 AtomicInteger 一样, 也是 int value, 但是用 1 表示 true, 用 0 表示 false, 且 方法只剩下个 getAndSet

数组类型原子类

针对数组提指定索引位置元素的无锁线程安全的方法

AtomicIntegerArray

内部维护了一个 int 数组, 将指定索引转换为数组中指定位置元素内存地址相对于数组对象内存地址的偏移, 然后针对该元素提供无锁线程安全的方法

package java.util.concurrent.atomic;
import java.util.function.IntUnaryOperator;
import java.util.function.IntBinaryOperator;
import sun.misc.Unsafe;

public class AtomicIntegerArray implements java.io.Serializable {
    private static final long serialVersionUID = 2862133569453604235L;

    private static final Unsafe unsafe = Unsafe.getUnsafe();
    // 获取数组对象中, 第一个元素的内存地址相对于数组对象内存地址的偏移
    // 对象 = 对象头 + 实例数据 + 对齐填充(8的倍数)
    // 对象头 = mark word + klass pointer + 数组长度(数组对象才有该部分)
    // Java规定数组中的元素都是相同类型,因此数组中的每个元素的内存大小是相同的,也就是说,只要知道数组的起始位置,我们就可以算出指定下标的数组元素的内存地址
    // position = base + index * size(element)
    private static final int base = unsafe.arrayBaseOffset(int[].class);
    private static final int shift;
    private final int[] array;

    static {
    	// 获取指定类型数组中每个元素占用内存的大小,int类型的是 4
        int scale = unsafe.arrayIndexScale(int[].class);
        if ((scale & (scale - 1)) != 0)
            throw new Error("data type scale not a power of two");
        // Integer.numberOfLeadingZeros(scale), 获取补码高位连续0的个数, Integer.numberOfLeadingZeros(4) - 29
        // int 4 转成2进制, 00000000 00000000 00000000 00000100, 前面连续的0的个数是29
        // shift = 2, 用于计算数组元素index与0到index的元素所占用的空间
        shift = 31 - Integer.numberOfLeadingZeros(scale);
    }

    private long checkedByteOffset(int i) {
        if (i < 0 || i >= array.length)
            throw new IndexOutOfBoundsException("index " + i);

        return byteOffset(i);
    }

	// 根据指定的数组索引获取对应元素的内存地址相对于数组对象内存地址的偏移
    private static long byteOffset(int i) {
    	// i左移2位, 相当于i*4
    	// 0, 左移2位, 0
    	// 1, 左移2位, 4
    	// 2, 左移2位, 8
    	// 等效于 position = base + index * size(element)
        return ((long) i << shift) + base;
    }

	// 获取指定元素(将索引转成偏移)
    public final int get(int i) {
        return getRaw(checkedByteOffset(i));
    }

	// 通过偏移的方式获取数组的某个元素(从主存拿最新的(volatile))
    private int getRaw(long offset) {
        return unsafe.getIntVolatile(array, offset);
    }

	// 通过偏移的方式设定数组的指定索引位的元素
    public final void set(int i, int newValue) {
        unsafe.putIntVolatile(array, checkedByteOffset(i), newValue);
    }

	// 非volatile方式设置指定索引位的元素
    public final void lazySet(int i, int newValue) {
        unsafe.putOrderedInt(array, checkedByteOffset(i), newValue);
    }

	// 通过index计算出偏移, 然后给数组对象的指定偏移处设置新值
    public final int getAndSet(int i, int newValue) {
        return unsafe.getAndSetInt(array, checkedByteOffset(i), newValue);
    }

	// 通过index计算出偏移, 然后给数组对象的指定偏移处设置新值(旧值增加delta)
    public final int getAndAdd(int i, int delta) {
        return unsafe.getAndAddInt(array, checkedByteOffset(i), delta);
    }

	// ...

}

AtomicLongArray

和 AtomicIntegerArray 一样, 就是 int 数组换成了 long 数组

AtomicReferenceArray

和 AtomicIntegerArray 一样, 就是 int 数组换成了 Object 数组, 因为不是 int long, 所以没有了 自增/自减 相关方法, 可通过 getAndUpdate 等自行实现想要的操作

unsafe.arrayIndexScale(Object[].class) = 4

引用类型原子类

AtomicReference

和 AtomicInteger 一样, 就是 int value 换成了泛型的 V value, 因为不是 int long, 所以没有了 自增/自减 相关方法

AtomicStampedReference

并发编程 — AtomicStampedReference 详解

CAS自旋有ABA问题, 针对乐观锁在并发情况下的操作,我们通常会增加版本号,比如数据库中关于乐观锁的实现方式,以此来解决并发操作带来的ABA问题, AtomicStampedReference 也是这么做的

AtomicStampedReference在构建的时候需要一个类似于版本号的int类型变量stamped,每一次针对共享数据的变化都会导致该 stamped 的变化(stamped 需要应用程序自身去负责,AtomicStampedReference并不提供,一般使用时间戳作为版本号),因此就可以避免ABA问题的出现,AtomicStampedReference的使用也是极其简单的,创建时我们不仅需要指定初始值,还需要设定stamped的初始值,在AtomicStampedReference的内部会将这两个变量封装成Pair对象

// 静态内部类,封装了 变量引用 和 版本号
private static class Pair<T> {
    final T reference;  // 变量引用
    final int stamp;    // 版本号
    private Pair(T reference, int stamp) {
        this.reference = reference;
        this.stamp = stamp;
    }
    static <T> Pair<T> of(T reference, int stamp) {
        return new Pair<T>(reference, stamp);
    }
}

// volatile 修饰的 Pair
private volatile Pair<V> pair;

public AtomicStampedReference(V initialRef, int initialStamp) {
    pair = Pair.of(initialRef, initialStamp);
}

public V getReference() {
    return pair.reference;
}

public int getStamp() {
    return pair.stamp;
}

// 期望的引用和邮戳与当前的引用和邮戳相同时, 更新为新的引用和邮戳 pair
public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) {
	// 拿到当前引用和邮戳的 pair
    Pair<V> current = pair;
    // 如果 期望引用不等于当前引用 且 期望邮戳不等于当前邮戳, 则不操作
    // 反之
    // 		如果新引用等于当前引用 且 新邮戳等于当前邮戳, 则直接返回true, 直接当做执行了比较设置
    //		反之, 执行 casPair, 并返回其执行结果
    return expectedReference == current.reference && expectedStamp == current.stamp &&
        (
        	(newReference == current.reference && newStamp == current.stamp)
        	||
        	// 这里的current是cas里的期望值,和this的offset处对象对比,相同才会set后面的新pair
        	casPair(current, Pair.of(newReference, newStamp))
        );
}

// 新 pair 和 旧 pair 中的 引用和邮戳, 有任何一个不相等, 才更新
public void set(V newReference, int newStamp) {
    Pair<V> current = pair;
    if (newReference != current.reference || newStamp != current.stamp)
        this.pair = Pair.of(newReference, newStamp);
}

// 期望的引用和当前的引用相同时, 更新新的邮戳(和期望的引用)
public boolean attemptStamp(V expectedReference, int newStamp) {
    Pair<V> current = pair;
    // 如果期望引用不等于当前引用, 则不操作
    // 反之
    // 		如果新邮戳等于当前邮戳, 直接返回true, 直接当做执行了比较设置
    //		反之, 执行 casPair, 并返回其结果
    return expectedReference == current.reference &&
        (
        	newStamp == current.stamp
        	|| 
         	casPair(current, Pair.of(expectedReference, newStamp))
         );
}

// cmp期望的pair, val新pair
private boolean casPair(Pair<V> cmp, Pair<V> val) {
    return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}

AtomicMarkableReference

和 AtomicStampedReference 一样, 只不过 Pair 里面的 int stemp 变成了 boolean mark, 解决ABA问题的效果算是介于 AtomicReference 和 AtomicStampedReference 中间

字段升级类型原子类

可以把某个类的某个 volatile 字段升级成为具有原子效果 get-set 的字段

AtomicIntegerFieldUpdater

基于反射的实用工具,可以对指定类的指定 volatile int 字段进行原子更新。该类中的其他字段都可以相互独立地原子更新

该类中 compareAndSet 的程度不如其他原子类, 因为对指定字段的更新, 除了通过该 updater 也可以通过原类原属性的 setter

要求属性必须是 int volatile, 不能是 static final, 且 类中的该字段 对于调用 AtomicIntegerFieldUpdater 的地方必须可见(同类中可以 private)

本质上还是CAS, this, offset, expect, update

@Test
@SneakyThrows
public void test() {

	@Data
	@AllArgsConstructor
	class Item {
		// 必须是 int volatile, 不能是 static final
		volatile int value;
	}

	Item item = new Item(1);

	AtomicIntegerFieldUpdater<Item> atomic = AtomicIntegerFieldUpdater.newUpdater(Item.class, "value");
	System.out.println(atomic.getAndIncrement(item));
	System.out.println(item);
}
1
Item(value=2)

AtomicLongFieldUpdater

和 AtomicIntegerFieldUpdater 类似, 可以堆指定类的指定 volatile long 字段进行原子更新

在 AtomicLong 里有一个静态常量 VM_SUPPORTS_LONG_CAS, 记录底层 JVM 是否支持 long 的无锁 compareAndSwap。虽然 Unsafe.compareAndSwapLong 方法在任何一种情况下都有效,但应该在 Java 级别处理一些构造以避免锁定用户可见的锁。

  • true, 使用 CASUpdater, 即 Unsafe.compareAndSwapLong
  • false, 使用 LockedUpdater, 即 synchronized (this) {}

AtomicReferenceFieldUpdater<T, V> T:对象类型, V:字段类型

和 AtomicIntegerFieldUpdater 类似, 可以堆指定类的指定 volatile 引用类型(非基础数据类型) 字段进行原子更新

@Test
@SneakyThrows
public void test() {

	@Data
	@AllArgsConstructor
	class Item {

		volatile String value;
	}

	Item item = new Item("1");

	AtomicReferenceFieldUpdater<Item, String> atomic = AtomicReferenceFieldUpdater.newUpdater(Item.class, String.class, "value");
	System.out.println(atomic.getAndSet(item, "2"));
	System.out.println(item);
}
1
Item(value=2)

Adder 累加器

LongAdder

在低争用下,AtomicLong和LongAdder这两个类具有相似的特征

但是在竞争激烈的情况下,LongAdder的预期吞吐量要高得多,只是要消耗更多的空间(但也有限), 空间换时间, LongAdder 把不同线程对应到不同的Cell上进行修改,降低了冲突的概率,有多段锁的理念

LongAdder适合的场景是统计求和的场景,而且LongAdder基本只提供了add方法,而AtomicLong还具有cas方法;

在这里插入图片描述
在这里插入图片描述

  • AtomicLong的实现原理是:每一次加法都需要做同步,所以在高并发的时候会导致冲突比较多,也就降低了效率;

  • LongAdder不需要在每一次计算的时候都同步,而是只有在最后求和的时候才需要把最后整个的值进行汇总

  • LongAdder,每个线程会有自己的一个计数器,仅用来在自己线程内计数,这样一来就不会和其他线程的计数器干扰

  • LongAdder 引入了分段累加的概念,内部有一个base变量和一个Cell[]数组共同参与计数

    • base变量:竞争不激烈,直接累加到该变量上
    • Cell[]数组:竞争激烈,各个线程分散累加自己的槽Cell[i]中
    • sum源码: base 与 各个cell 的值求和, 和就是总的计数结果
@Test
@SneakyThrows
public void test() {

	LongAdder counter = new LongAdder();
	ExecutorService pool = Executors.newFixedThreadPool(16);
	long time = System.currentTimeMillis();
	for (int i = 0; i < 10000; i++) {
		pool.submit(() -> {
			for (int j = 0; j < 10000; j++) {
				counter.increment();
			}
		});
	}
	pool.shutdown();
	pool.awaitTermination(1, TimeUnit.HOURS);
	System.out.println(counter.sum());
	System.out.println(System.currentTimeMillis() - time);

	AtomicLong counter2 = new AtomicLong();
	ExecutorService pool2 = Executors.newFixedThreadPool(16);
	long time2 = System.currentTimeMillis();
	for (int i = 0; i < 10000; i++) {
		pool2.submit(() -> {
			for (int j = 0; j < 10000; j++) {
				counter2.incrementAndGet();
			}
		});
	}
	pool2.shutdown();
	pool2.awaitTermination(1, TimeUnit.HOURS);
	System.out.println(counter2.get());
	System.out.println(System.currentTimeMillis() - time2);

}
100000000
125
100000000
1218

DoubleAdder

和 DoubleAdder 一样, 但是因为底层的 Cell 数组内部维护的是 long 而非 double, 所以涉及到了 long 和 double 的转换

  • Double.doubleToRawLongBits: 将 double 转换为 按位展示的 IEEE 754 标准的双精度浮点数, double 和 long 位数相同, 可以转换
  • Double.longBitsToDouble: 相反

Accumulator 累加器

Accumulator 和 Adder 非常相似,Accumulator 就是一个更通用版本的 Adder

注意: 线程内或跨线程的累积顺序无法保证且不能依赖,因此此类仅适用于累积顺序无关紧要的函数, 类似于加法交换律

LongAccumulator

构造函数如下

// accumulatorFunction - 二元的LongOperator, 即输入两个long返回long的Function
// identity – 累加器函数的标识(初始值)
public LongAccumulator(LongBinaryOperator accumulatorFunction, long identity)

如何理解这两个参数?

  • identity 是初始值, 即x的初始值
  • LongBinaryOperator 代表 long method(long x, long y), 其中x代表当前值, y代表给出的更新值, accumulate(long y) 方法的入参就是这里的y

new LongAdder() 等效于 new LongAccumulator((x, y) -> x + y, 0L)

举例

  • new LongAccumulator((x, y) -> x + y, 0L), new 相当于执行 x=0, 执行 accumulate(y) 相当于执行 x=x+y
  • new LongAccumulator((x, y) -> Math.min(x, y), 0), new 相当于执行 x=0, 执行 accumulate(y) 相当于执行 x=Math.min(x,y)
@Test
@SneakyThrows
public void test() {

	LongAccumulator accumulator = new LongAccumulator((x, y) -> x + y, 0L);
	accumulator = new LongAccumulator(Long::sum, 0L);

	for (int i = 1; i <= 100; i++) {
		accumulator.accumulate(i);
	}

	System.out.println(accumulator.get());

}
5050

DoubleAccumulator

和 LongAccumulator 一样

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值