Java 并发编程:JUC 包中原子操作类的原理和用法

通过上一部分的分析,我们应该基本理解了 CAS 的无锁思想,并对"魔法类" Unsafe 有了更全面的了解。这也是我们分析原子包的前提。

接下来,让我们一步步分析 CAS 在 Java 中的应用。JDK5 之后,JUC 包提供了 java.util.concurrent.atomic包,该包下提供了大量基于 CAS 的原子操作类。如果在未来你不希望对程序代码加锁,但又想避免线程安全问题,那么可以使用该包下提供的类。Atomic 包提供的操作类主要可以分为以下四种类型:

  • 基本类型原子操作类
  • 引用类型原子操作类
  • 数组类型原子操作类
  • 属性更新原子操作类

1. 基本类型原子操作类

Atomic 包提供的基本类型原子操作类有 AtomicInteger、AtomicBoolean 和 AtomicLong。它们的底层实现方式和使用方式是一样的。

所以我们只以其中一个------AtomicInteger 为例进行分析。AtomicInteger 主要用于对 int 类型的数据执行原子操作。它提供了原子自增方法、原子自减方法以及原子赋值方法等 API。

这里结合源码,我提供了一些注释:

csharp 复制代码
public class AtomicInteger extends Number implements java.io.Serializable {
    privatestaticfinallong serialVersionUID = 6214790243416807050L;

    // 获取 Unsafe 类的实例
    privatestaticfinal Unsafe unsafe = Unsafe.getUnsafe();

    // 变量 value 在 AtomicInteger 实例对象内的内存偏移量
    privatestaticfinallong valueOffset;

    static {
        try {
           // 通过 Unsafe 类的 objectFieldOffset() 方法获取 value 变量在对象内存中的偏移量
           // 通过这个偏移量 valueOffset,Unsafe 类的内部方法可以访问该变量 value 以进行读写操作
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { thrownew Error(ex); }
    }
   // 当前 AtomicInteger 封装的 int 变量 value
    privatevolatileint value;

    public...ment() {
        return unsafe.getAndAddInt(this, valueOffset, -1);
    }
   // 当前值增加 delta 并返回旧值。底层 CAS 操作
    public final int getAndAdd(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }
    // 当前值增加 1 并返回增加后的新值。底层 CAS 操作
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    // 当前值减少 1 并返回减少后的新值。底层 CAS 操作
    public final int decrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
    }
   // 当前值增加 delta 并返回新值。底层 CAS 操作
    public final int addAndGet(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
    }
   // 省略了一些不常用的方法....
}

从上述源码可知,AtomicInteger 原子类的所有方法中,没有使用任何互斥锁机制来实现同步,而是通过前面介绍的 Unsafe 类提供的 CAS 操作来保证线程安全。在我们之前的分析文章中提到,count++ 实际上存在线程安全问题。那么让我们观察一下 AtomicInteger 原子类中的自增实现 incrementAndGet:

csharp 复制代码
public final int incrementAndGet() {
   return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

从上述源码可以看出,最终的实现是调用 unsafe.getAndAddInt()方法来完成的。正如我们在之前的分析中所学到的,这个方法是 JDK8 之后基于原始 CAS 操作的新方法。我们进一步跟进:

arduino 复制代码
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!compareAndSwapInt(o, offset, v, v + delta));
    return v;
}

可以看出,getAndAddInt通过一个 do-while 死循环不断重试更新要设置的值,直到成功为止。其中调用了 Unsafe 类中的 compareAndSwapInt方法,这是一个 CAS 操作方法。注意:在 Java 8 之前的源码实现中,这里是一个 for 循环,并且自增逻辑是直接在 AtomicInteger 类中实现的。在 Java 8 中,它被替换为 while 循环并移动到了 Unsafe 类中实现。

下面让我们通过一个简单的小 Demo 来掌握基本类型原子操作类的用法:

ini 复制代码
public class AtomicIntegerDemo {

    static AtomicInteger atomicInteger = new AtomicInteger();

    publicstaticclass AddThread implements Runnable {
        public void run(){
           for(int i = 0; i < 10000; i++)
               atomicInteger.incrementAndGet();
        }

    }
    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[11];
        // 开启10个线程同时对 atomicInteger 进行自增操作
        for(int i = 1; i <= 10; i++) {
            threads[i]=new Thread(new AddThread());
        }

        for(int i = 1; i <= 10; i++) {
            threads[i].start();
        }
         // 等待所有线程执行完毕再输出结果
        for(int i = 1; i <= 10; i++) {
            threads[i].join();
        }

        System.out.println(atomicInteger);
    }
}

输出:100000

在上面的 Demo 中,使用了 AtomicInteger 代替了原始的 int。这样,无需加锁即可保证线程安全。AtomicBoolean 和 AtomicLong 就不再分析了,其用法和原理是一样的。

2. 引用类型原子操作类

引用类型的原子操作类主要分析 AtomicReference 类。其他的原理和用法也都一样。

让我们直接看一个例子:

typescript 复制代码
public class AtomicReferenceDemo {
    publicstatic AtomicReference<Student> atomicStudentRef = new AtomicReference<>();

    public static void main(String[] args) {
        Student s1 = new Student("Tom", 18);
        atomicStudentRef.set(s1);
        Student s2 = new Student("Jerry", 20);
        atomicStudentRef.compareAndSet(s1, s2);
        System.out.println(atomicStudentRef.get().toString());
    }

    @AllArgsConstructor
    staticclass Student {

        private String name;
        privateint age;

        @java.lang.Override
        public java.lang.String toString() {
            return"Student{" +
                    "name='" + name + ''' +
                    ", age=" + age +
                    '}';
        }
    }
}

输出:

ini 复制代码
Student{name='Jerry', age=20}

在此之前,我们分析了原子计数器 AtomicInteger 类的底层实现是通过 Unsafe 类中的 CAS 操作完成的。那么我们要分析的 AtomicReference 底层又是如何实现的呢?

typescript 复制代码
public class AtomicReference<V> implements java.io.Serializable {
    // 获取 Unsafe 对象实例
    privatestaticfinal Unsafe unsafe = Unsafe.getUnsafe();
    // 定义 value 偏移量
    privatestaticfinallong valueOffset;

    static {
        try {
            /* 静态代码块在类加载时为 offset 赋值。通过 Unsafe 类提供的 API 获取类属性的地址,从而获取当前类中定义的属性 value 的地址 */
            valueOffset = unsafe.objectFieldOffset
                (AtomicReference.class.getDeclaredField("value"));
        } catch (Exception ex) { thrownew Error(ex); }
    }
    // 内部变量 value。Unsafe 类可以通过 valueOffset 内存偏移量获取该变量
    privatevolatile V value;

    /* 原子替换方法,间接调用 Unsafe 类的 compareAndSwapObject(),
    这是一个实现了 CAS 操作的本地方法 */
    public final boolean compareAndSet(V expect, V update) {
        return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
    }

    // 设置并获取旧值
    public final V getAndSet(V newValue) {
        return (V)unsafe.getAndSetObject(this, valueOffset, newValue);
    }
    // 省略代码......
}

// Unsafe 类中的 getAndSetObject 方法在调用时实际上仍然是一个 CAS 操作
public final Object getAndSetObject(Object o, long offset, Object newValue) {
      Object v;
      do {
          v = getObjectVolatile(o, offset);
      } while (!compareAndSwapObject(o, offset, v, newValue));
      return v;
  }

从上述源码可知,AtomicReference 与我们之前分析的 AtomicInteger 原理大体相同,最终也都是通过 Unsafe 类提供的 CAS 操作实现的。AtomicReference 其他方法的实现原理也大致相同。

不过,需要注意的是 Java 8 为 AtomicReference 增加了几个新的 API:

  • getAndUpdate(UnaryOperator)
  • updateAndGet(UnaryOperator)
  • getAndAccumulate(V, AnaryOperator)
  • accumulateAndGet(V, AnaryOperator)

上述方法几乎存在于所有原子类中,这些方法可以在执行 CAS 操作之前,基于 Lambda 表达式对期望值或要更新的值执行其他操作。简单来说,就是在执行 CAS 更新之前,对期望值或要更新的值进行额外的修改。

3. 数组类型原子操作类

数组类型原子操作类的含义其实很简单,指的是以原子的形式更新数组中的元素,以避免线程安全问题。JUC 包中的数组类型原子操作类具体分为以下三个类:

  • AtomicIntegerArray
  • AtomicLongArray
  • AtomicReferenceArray

同样,我们只分析单个类,其他两个大致相同。这里我们以 AtomicIntegerArray 为例进行分析:

ini 复制代码
public class AtomicIntegerArrayDemo {
    static AtomicIntegerArray atomicArr = new AtomicIntegerArray(10);

    publicstaticclass incrementTask implements Runnable {
        public void run() {
            for (int i = 0; i < 10000; i++) {
                atomicArr.getAndIncrement(i % atomicArr.length());
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(new incrementTask());
        }
        for (int i = 0; i < 10; i++) {
            threads[i].start();
        }
        for (int i = 0; i < 10; i++) {
            threads[i].join();
        }
        System.out.println(atomicArr);
    }
}

输出:

csharp 复制代码
[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]

在上面的 Demo 中,我们开启了 10 个线程对数组中的元素执行自增操作,执行结果符合我们的预期。接下来让我们一步步分析 AtomicIntegerArray 的内部实现逻辑。源码如下:

csharp 复制代码
public class AtomicIntegerArray implements java.io.Serializable {
    // 获取 Unsafe 实例对象
    privatestaticfinal Unsafe unsafe = Unsafe.getUnsafe();
    // 正如我们之前在 Unsafe 类中分析的那样,arrayBaseOffset() 的功能是:获取数组第一个元素的内存起始地址
    privatestaticfinalint base = unsafe.arrayBaseOffset(int[].class);

    privatestaticfinalint shift;
    // 内部数组
    privatefinalint[] array;

    static {
        // 获取数组中一个元素占用的内存空间
        int scale = unsafe.arrayIndexScale(int[].class);
        // 检查是否为 2 的幂次方,否则抛出异常
        if ((scale & (scale - 1))!= 0)
            thrownew Error("data type scale not a power of two");
        //
        shift = 31 - Integer.numberOfLeadingZeros(scale);
    }

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

        return byteOffset(i);
    }
    // 计算数组中每个元素的内存地址
    private static long byteOffset(int i) {
        return ((long) i << shift) + base;
    }
    // 省略代码......
}

在之前对 Unsafe 的分析中,我们了解到 arrayBaseOffset可以获取数组中第一个元素的内存起始位置,我们还有一个 API:arrayIndexScale可以获取数组中指定下标元素占用的内存空间大小。目前,AtomicIntegerArray 是 int 类型的。我们在学习 Java 基础时知道,int 的大小在内存中占用 4 个字节,所以 scale 的值为 4。那么现在,如果我们根据这两条信息来计算数组中每个元素的内存起始地址呢?稍微推导一下,我们可以得出以下结论:

数组中每个元素的内存起始位置 = 数组第一个元素的起始位置 + 数组元素的下标 _ 数组中每个元素占用的内存空间

而上述源码中的 byteOffset(i)方法就是上述公式的体现。有些人看到这里可能会感到困惑。这个方法的实现似乎与我刚才描述的结论不一致。别担心,让我们先看看 shift的值是如何获取的:

shift = 31 --- Integer.numberOfLeadingZeros(scale);

Integer.numberOfLeadingZeros(scale):计算 scale 的前导零数量(转换为二进制后前面连续 0 的个数称为前导零)。scale = 4,转换为二进制是 00000000 00000000 00000000 00000100,所以前导零数量是 29,因此 shift 值为 2。

那么回到我们需要解决的问题:如何计算数组中每个元素的内存起始位置?我们利用刚刚得到的 shift 值,将其应用到 byteOffset(i)方法中,来计算数组中每个元素的内存起始位置(前提是数组下标不越界):

byteOffset 方法体:(i << shift) + base (i 是索引,即数组元素的下标,base 是数组第一个元素的内存起始位置)
第一个元素:memoryAddress = 0 << 2 + base,即 memoryAddress = base + 0 * 4

第二个元素:memoryAddress = 1 << 2 + base,即 memoryAddress = base + 1 * 4

第三个元素:memoryAddress = 2 << 2 + base,即 memoryAddress = base + 2 * 4

第四个元素:memoryAddress = 3 << 2 + base,即 memoryAddress = base + 3 * 4

其他元素的位置以此类推......

以上描述就是 AtomicIntegerArray.byteOffset方法的原理。所以 byteOffset(int)方法可以根据数组下标计算每个元素的内存地址。

AtomicIntegerArray 中的其他方法都是通过间接调用 Unsafe 类的 CAS 原子操作方法实现的。让我们简单看几个常用的方法:

arduino 复制代码
// 执行自增操作并返回旧值。入参 i 是索引,即数组元素的下标
public final int getAndIncrement(int i) {
      return getAndAdd(i, 1);
}
// 对指定下标的元素执行自增操作并返回新值
public final int incrementAndGet(int i) {
    return getAndAdd(i, 1) + 1;
}
// 对指定下标的元素执行自减操作并返回新值
public final int decrementAndGet(int i) {
    return getAndAdd(i, -1) - 1;
}
// 间接调用 unsafe.getAndAddInt() 方法
public final int getAndAdd(int i, int delta) {
    return unsafe.getAndAddInt(array, checkedByteOffset(i), delta);
}
// Unsafe 类中的 getAndAddInt 方法执行 CAS 操作
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!compareAndSwapInt(o, offset, v, v + delta));
    return v;
}

4. 属性更新原子操作类

如果我们只需要某个类的某个字段(类的属性)也变成原子操作,我们可以使用属性更新原子操作类。

例如,某时刻项目需求发生变化,然后需求再次变更,导致某个类中的变量需要多线程操作。由于这个变量在多处使用,修改起来比较麻烦,而且原本使用的地方不需要线程安全,只有新场景需要使用。我们可以借助原子更新器来处理这种场景。Atomic 并发包提供了以下三个类:

  • AtomicIntegerFieldUpdater:用于更新整型属性的原子操作类
  • AtomicLongFieldUpdater:用于更新长整型属性的原子操作类
  • AtomicReferenceFieldUpdater:用于更新引用类型中属性的原子操作类

但是,值得注意的是,使用原子更新类的条件比较严格,如下:

  • 被操作的字段不能被 static 修饰。
  • 被操作的字段不能被 final 修饰,因为常量无法修改。
  • 被操作的字段必须被 volatile 修饰以保证可见性,即必须保证数据的读取是线程可见的。
  • 属性必须对 Updater 所在的当前区域可见。如果原子更新器操作不在当前类中执行,则不能使用 private 和 protected 修饰符。当子类操作父类时,修饰符必须是 protected 权限或以上。如果在同一个包中,必须是 default 权限或以上。也就是说,要时刻保证操作类与被操作类之间的可见性。

让我们看一下 AtomicIntegerFieldUpdater 和 AtomicReferenceFieldUpdater 的简单使用方法:

typescript 复制代码
import lombok.AllArgsConstructor;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

publicclass AtomicIntegerFieldUpdaterDemo {

    publicstaticclass Candidate {
        int id;
        volatileint score;
    }

    static AtomicIntegerFieldUpdater<Candidate> atIntegerUpdater
            = AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score");

    static AtomicReferenceFieldUpdater<Student, String> atRefUpdate =
            AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name");

    // 用于验证分数是否正确
    publicstatic AtomicInteger allScore = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        final Candidate stu = new Candidate();
        Thread[] t = new Thread[10000];
        // 开启 10,000 个线程
        for (int i = 0; i < 10000; i++) {
            t[i] = new Thread() {
                public void run() {
                    if (Math.random() > 0.5) {
                        atIntegerUpdater.incrementAndGet(stu);
                        allScore.incrementAndGet();
                    }
                }
            };
            t[i].start();
        }

        for (int i = 0; i < 10000; i++) {
            t[i].join();
        }
        System.out.println("final score=" + stu.score);
        System.out.println("check all score=" + allScore);

        // AtomicReferenceFieldUpdater 简单使用
        Student student = new Student("Jerry", 17);
        atRefUpdate.compareAndSet(student, student.name, "Jerry-1");
        System.out.println(student);
    }

    @AllArgsConstructor
    staticclass Student {

        private String name;
        privateint age;

        @java.lang.Override
        public java.lang.String toString() {
            return"Student{" +
                    "name='" + name + ''' +
                    ", age=" + age +
                    '}';
        }
    }
}

我们使用 AtomicIntegerFieldUpdater 来更新候选人(Candidate)的分数。启动 10,000 个线程进行投票。当随机值大于 0.5 时,算作一票,分数自增一次。其中,allScore 用于验证分数是否正确(实际用于验证 AtomicIntegerFieldUpdater 更新的字段是否线程安全)。当 allScore 与 score 相同时,说明投票结果正确,也代表 AtomicIntegerFieldUpdater 能正确更新字段 score 的值并且是线程安全的。

对于 AtomicReferenceFieldUpdater,我们在代码中简单演示了它的使用方法。注意在指定 AtomicReferenceFieldUpdater 的泛型时,需要两个泛型参数,一个是修改类的类型,另一个是修改字段的类型。

至于 AtomicLongFieldUpdater,它与 AtomicIntegerFieldUpdater 类似,不再赘述。接下来简单了解一下 AtomicIntegerFieldUpdater 的实现原理。它实际上是反射与 Unsafe 类的结合。AtomicIntegerFieldUpdater 是一个抽象类,实际的实现类是 AtomicIntegerFieldUpdaterImpl:

csharp 复制代码
public abstract class AtomicIntegerFieldUpdater<T> {
    public static <U> AtomicIntegerFieldUpdater<U> newUpdater(Class<U> tclass, String fieldName){
        return new AtomicIntegerFieldUpdaterImpl<U>
            (tclass, fieldName, Reflection.getCallerClass());
    }
 }

不难从源码中发现,实际上 AtomicIntegerFieldUpdater 的定义是一个抽象类,最终的实现类是 AtomicIntegerFieldUpdaterImpl。

让我们进一步查看 AtomicIntegerFieldUpdaterImpl 的源码。我提供了一些注释:

scss 复制代码
private staticclass AtomicIntegerFieldUpdaterImpl<T>
            extends AtomicIntegerFieldUpdater<T> {
    // 获取 unsafe 实例
    privatestaticfinal Unsafe unsafe = Unsafe.getUnsafe();
    // 定义内存偏移量
    privatefinallong offset;
    privatefinal Class<T> tclass;
    privatefinal Class<?> cclass;

    // 构造方法
    AtomicIntegerFieldUpdaterImpl(final Class<T> tclass,
                                  final String fieldName,
                                  final Class<?> caller) {
        final Field field;// 要修改的字段
        finalint modifiers;// 字段修饰符
        try {
            field = AccessController.doPrivileged(
                new PrivilegedExceptionAction<Field>() {
                    public Field run() throws NoSuchFieldException {
                        // 通过反射获取 Field 对象
                        return tclass.getDeclaredField(fieldName);
                    }
                });
                // 获取字段修饰符
            modifiers = field.getModifiers();
            // 检查字段的访问权限,如果不在访问范围内则抛出异常。
            sun.reflect.misc.ReflectUtil.ensureMemberAccess(
                caller, tclass, null, modifiers);
            // 获取对应类对象的类加载器
            ClassLoader cl = tclass.getClassLoader();
            ClassLoader ccl = caller.getClassLoader();
            if ((ccl != null) && (ccl != cl) &&
                ((cl == null) || !isAncestor(cl, ccl))) {
          sun.reflect.misc.ReflectUtil.checkPackageAccess(tclass);
            }
        } catch (PrivilegedActionException pae) {
            thrownew RuntimeException(pae.getException());
        } catch (Exception ex) {
            thrownew RuntimeException(ex);
        }

        Class<?> fieldt = field.getType();
        // 判断类型是否为 int
        if (fieldt != int.class)
            throw new IllegalArgumentException("Must be integer type");
        // 判断是否被 volatile 修饰
        if (!Modifier.isVolatile(modifiers))
            thrownew IllegalArgumentException("Must be volatile type");

        this.cclass = (Modifier.isProtected(modifiers) &&
                       caller != tclass) ? caller : null;
        this.tclass = tclass;
        // 获取字段在对象内存中的偏移量,并使用内存偏移量获取或修改字段的值。
        offset = unsafe.objectFieldOffset(field);
    }
}

从 AtomicIntegerFieldUpdaterImpl 的源码可以看出,实际上字段更新器都是通过反射机制和 Unsafe 类实现的。

让我们看一下 AtomicIntegerFieldUpdaterImpl 中自增方法 incrementAndGet 的实现:

csharp 复制代码
public int incrementAndGet(T obj) {
    int prev, next;
    do {
        prev = get(obj);
        next = prev + 1;
        // 调用下面的 compareAndSet 方法完成 cas 操作
    } while (!compareAndSet(obj, prev, next));
    return next;
}

// 最终调用的是 Unsafe 类的 compareAndSwapInt() 方法。
public boolean compareAndSet(T obj, int expect, int update) {
        if (obj == null || obj.getClass() != tclass || cclass != null) fullCheck(obj);
        return unsafe.compareAndSwapInt(obj, offset, expect, update);
}

我们在这里可以发现,实际上 J.U.C 中 Atomic 包下原子类的最终实现都是通过前面分析过的 Unsafe 类完成的。包括后面我们要讲到的许多并发知识,如 AQS 等,都会涉及到 CAS 机制。敬请期待~

相关推荐
源代码•宸36 分钟前
Golang面试题库(Context、Channel)
后端·面试·golang·context·channel·sudog·cancelctx
橘子师兄41 分钟前
C++AI大模型接入SDK—Ollama本地接入Deepseek
c++·人工智能·后端
sheji34161 小时前
【开题答辩全过程】以 基于Spring Boot的旅游推荐系统的设计与实现为例,包含答辩的问题和答案
spring boot·后端·旅游
只是懒得想了1 小时前
Go语言ORM深度解析:GORM、XORM与entgo实战对比及最佳实践
开发语言·数据库·后端·golang
爱吃山竹的大肚肚1 小时前
异步导出方案
java·spring boot·后端·spring·中间件
三水不滴1 小时前
从原理、场景、解决方案深度分析Redis分布式Session
数据库·经验分享·redis·笔记·分布式·后端·性能优化
Hx_Ma161 小时前
SpringMVC框架(上)
java·后端
晚风_END1 小时前
postgresql数据库|连接池中间件pgbouncer的部署和配置详解
数据库·后端·spring·postgresql·中间件·个人开发
sheji34162 小时前
【开题答辩全过程】以 基于Spring Boot的化妆品销售系统的设计与实现为例,包含答辩的问题和答案
java·spring boot·后端
只是懒得想了2 小时前
Go服务限流实战:基于golang.org/x/time/rate与uber-go/ratelimit的深度解析
开发语言·后端·golang