2 Atomic 类

felix.shao2025-02-16

2 Atomic 类

2.1 AtomicInteger 和 AtomicLong

2.1.1 悲观锁和乐观锁

  • 悲观锁:数据发生并发冲突的概率很大,所以读操作之前就上锁。数据库中的行锁、表锁、读锁、写锁,以及 synchronized、ReentrantLock 都是悲观锁。
  • 乐观锁:数据发生并发冲突的概率比较小,所以读操作之前不上锁。等到写操作的时候,再判断数据在此期间是否被其他线程修改了。如果被其他线程修改了,就把数据重新读出来,重复该过程;如果没有修改,就写回去。判断数据是否被修改,同时写会新值,这两个操作要合成一个院子操作,也就是 CAS(Compare And Set)。

 AtomicInteger 的实现就是典型的乐观锁,在 Mysql 和 Redis 中有类似的思路。

2.1.2 Unsafe 的 CAS 详解

 CAS 乐观锁函数,就是封装的 Unsafe 类中的一个 native 函数,如下源码。

public final class Unsafe {
    /**
	 * var1 对象
	 * var2 对象的成员变量(long类型的内存偏移量表示该位置)
	 * var4 变量的旧值
	 * var5 变量的新值
	 */
	public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
    public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

	/**
	 * 对象字段的内存偏移量计算方法
	 */
	public native long objectFieldOffset(Field var1);
}

2.1.3 自旋与阻塞

 当一个线程拿不到锁的时候,有以下两种基本的等待策略。

  1. 阻塞:放弃 CPU,进入阻塞状态,等待后续被唤醒,再重新被操作系统调度。
  2. 自旋:不放弃 CPU,空转,不断重试。

 单核 CPU,只能用阻塞,因为如果不放弃 CPU,那么其他线程无法运行,也就无法释放锁。
 多 CPU 或者多核,自旋没有切换线程的开销,会很有用。
 AtomicInteger 的实现就用的是“自旋”策略,如果拿不到锁,就会一直重试。
 这两种策略并不是互斥的,可以结合使用,如果拿不到锁,先自旋几圈;如果自旋还拿不到锁,再阻塞,synchronized 关键字就是这样的实现策略。

2.2 AtomicBoolean 和 AtomicReference

 基本同 AtomicInteger。

2.2.1 为什么需要 AtomicBoolean

 因为要实现类似 CAS 比较和设值的原子操作。AtomicReference同理。

TIP

 注意 volatile 不是原子性的,因此需要使用 CAS,多个线程示例代码见 com/study/java/concurrent/chapter02/VolatileTest.java

2.2.2 如何支持 boolean 和 double 类型

 AtomicBoolean 类型调用 Unsafe 函数时,是将 Boolean 转为了 int 类型。
 AtomicDouble 类型调用 Unsafe 函数时,是依赖 double 类型提供的一堆 double 类型和 long 类型互转的函数,后续 DoubleAdder 时会详细说明。

2.3 AtomicStampedReference 与 AtomicMarkableReference

2.3.1 ABA 问题与解决办法

 CAS 都是基于“值”来做比较的。如果另外一个线程把变量的值从 A 改为 B,再从 B 改回到 A,那么尽管修改过两次,可是在当前线程做 CAS 操作的时候,却会因为值没变而任务数据没有被其他线程修改过,这就所谓的 ABA 问题。
 ABA 问题解决办法:不仅要比较“值”,还要比较“版本号”。AtomicStampedReference 就是这么做的,以下是源码。

public class AtomicStampedReference{
	public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
			// 值不变直接返回,值变化则复用原有的 cas,obj 里面存放了数据和版本号
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }
}

2.3.2 为什么没有 AtomicStampedInteger 或 AtomicStampedLong

 如 AtomicStampedReference.compareAndSet 源码,比较的是 Pair 里面有值和版本号。保证原子 CAS 操作就没有 AtomicStampedInteger 等了。

2.3.3 AtomicMarkableReference

 AtomicMarkableReference 和 AtomicStampedReference 的区别是,前者的版本号是 int 型,后者的是 boolean 型。因此后者并不能完全避免 ABA 问题,只是降低了 ABA 发生的概率。

2.4 AtomicIntegerFieldUpdater、AtomicLongFieldUpdater 和 AtomicReferenceFieldUpdater

2.4.1 为什么需要 AtomicXXXFieldUpdater

 对已有的类,在不更改其源代码的情况下,实现对其成员变量的原子操作。示例代码见 com/study/java/concurrent/chapter02/AtomicIntegerFieldUpdaterTest.java

2.4.2 限制条件

 查看 AtomicIntegerFieldUpdater 源码,限制了变量为 int 和 volatile 类型。其他 AtomicXXXFieldUpdater 也有类似的限制。

AtomicIntegerFieldUpdaterImpl(final Class<T> tclass,
                                      final String fieldName,
                                      final Class<?> caller) {
            if (field.getType() != int.class)
                throw new IllegalArgumentException("Must be integer type");

            if (!Modifier.isVolatile(modifiers))
                throw new IllegalArgumentException("Must be volatile type");

        }

2.5 AtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray

 concurrent 包提供了 AtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray 三个数组元素的原子操作。注意是:仅针对数组中一个元素的原子操作而言。

2.5.1 使用方式

 见下面源码。

public class AtomicIntegerArray{
	 public final boolean compareAndSet(int i, int expect, int update) {
        return compareAndSetRaw(checkedByteOffset(i), expect, update);
    }
}

2.5.2 实现原理

 还是使用的 Unsafe,只是将第二个参数的偏移地址改为了 i * scale + base 的位移运算。

2.6 Striped64 与 LongAdder

 从 JDK8 开始,针对 Long 型的原子操作,Java 又提供了 LongAdder、LongAccumulator;针对 Double 类型,Java 提供了 DoubleAdder、DoubleAccumulator。

TIP

 为什么要提供 LongAdder(其他三个同理)?

2.6.1 LongAdder 原理

 AtomicLong 内部是一个 volatile long 型变量,多个线程同时对一个变量进行 CAS 操作,在高并发场景下还是不够快。而 LongAdder 把一个变量拆成多份,变为多个变量(base 和多个 cell),并发度高时,平摊到这些 Cell 上,在最后取值的时候,再把 base 和这些 Cell 求 sum 运算,有些类似 ConcurrentHashMap 的分段锁的例子。
 英文 Striped 意为“条带”,也就是分片。它是最终一致性,不是强一致性的。

2.6.2 最终一致性

 在 sum 求和函数中,并没有对 cells 数组加锁,是有线程边求和,有线程修改数组里的值,即中途过程不是强一致性的,但是最终结果是一致的。

2.6.3 伪共享与缓存行填充

 与 CPU 架构有关系,JDK 8 对应的注解是 @sun.misc.Contented,LongAdder 使用其主要是为了不让 Cell[] 数组中相邻的元素落到同一个缓存行里。

2.6.4 LongAdder 核心实现

 详见 LongAdder 源码,核心方法是 add、longAccumulate 等方法。

2.6.5 LongAccumulator

 LongAccumulator 的原理和 LongAdder 类似,只是功能更强大,即多了个二元操作符。

2.6.6 DoubleAdder 与 DoubleAccumulator

 DoubleAdder 是转为了 Long 类型,然后其他逻辑同 LongAdder 了。
 DoubleAccumulator 也是 Striped64 的成员函数,和 longAccumulate 类似,也是多了 long 类型和 double 类型的互相转换。

Last Updated 2/16/2025, 4:13:06 PM