MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java中的原子操作类:AtomicInteger

2023-09-076.4k 阅读

一、Java 并发编程中的原子性问题

在多线程编程场景下,原子性是一个至关重要的概念。所谓原子性操作,是指不可被中断的一个或一系列操作。在 Java 中,一些简单的操作,如赋值操作 int i = 10;,在单线程环境下是原子性的,但在多线程环境中情况就变得复杂起来。

考虑如下场景,有两个线程同时对一个共享变量进行操作:

public class NonAtomicExample {
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                count++;
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                count++;
            }
        });

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println("Final count: " + count);
    }
}

理论上,如果 count++ 操作是原子性的,两个线程各执行 10000 次自增操作,最终的 count 值应该是 20000。但实际上运行这段代码,会发现每次输出的结果并不一定是 20000,往往会小于 20000。这是因为 count++ 操作并非原子性的,它实际上包含了三个步骤:读取 count 的值、对值进行加 1 操作、将新值写回 count。在多线程环境下,这三个步骤可能会被其他线程中断,导致数据不一致。

二、AtomicInteger 类概述

为了解决上述原子性问题,Java 提供了一系列原子操作类,AtomicInteger 就是其中之一。AtomicInteger 位于 java.util.concurrent.atomic 包下,它提供了一种可以在多线程环境下安全地对整数进行操作的方式。

AtomicInteger 的核心原理是利用了 CPU 提供的原子操作指令,通过硬件层面的支持来保证操作的原子性。在 Java 中,它主要依赖于 sun.misc.Unsafe 类来实现底层的原子操作。Unsafe 类提供了一些可以直接操作内存和硬件的方法,AtomicInteger 正是借助这些方法来实现高效的原子操作。

三、AtomicInteger 的常用方法

  1. int get() 该方法用于获取 AtomicInteger 当前的值。它是一个原子操作,不会受到其他线程的干扰。
AtomicInteger atomicInteger = new AtomicInteger(5);
int value = atomicInteger.get();
System.out.println("Current value: " + value);
  1. void set(int newValue) 用于设置 AtomicInteger 的值为指定的 newValue。同样,这也是一个原子操作。
AtomicInteger atomicInteger = new AtomicInteger(5);
atomicInteger.set(10);
System.out.println("New value: " + atomicInteger.get());
  1. int incrementAndGet() 先将 AtomicInteger 的值加 1,然后返回加 1 后的值。这个操作是原子性的,等同于 ++i 操作。
AtomicInteger atomicInteger = new AtomicInteger(5);
int result = atomicInteger.incrementAndGet();
System.out.println("Incremented value: " + result);
  1. int getAndIncrement() 先返回 AtomicInteger 当前的值,然后将其值加 1。此操作也是原子性的,等同于 i++ 操作。
AtomicInteger atomicInteger = new AtomicInteger(5);
int oldValue = atomicInteger.getAndIncrement();
System.out.println("Old value: " + oldValue);
System.out.println("New value: " + atomicInteger.get());
  1. int decrementAndGet() 先将 AtomicInteger 的值减 1,然后返回减 1 后的值。原子性操作,等同于 --i 操作。
AtomicInteger atomicInteger = new AtomicInteger(5);
int result = atomicInteger.decrementAndGet();
System.out.println("Decremented value: " + result);
  1. int getAndDecrement() 先返回 AtomicInteger 当前的值,然后将其值减 1。原子性操作,等同于 i-- 操作。
AtomicInteger atomicInteger = new AtomicInteger(5);
int oldValue = atomicInteger.getAndDecrement();
System.out.println("Old value: " + oldValue);
System.out.println("New value: " + atomicInteger.get());
  1. int addAndGet(int delta)AtomicInteger 的值加上指定的 delta,然后返回相加后的值。原子性操作。
AtomicInteger atomicInteger = new AtomicInteger(5);
int result = atomicInteger.addAndGet(3);
System.out.println("Added value: " + result);
  1. int getAndAdd(int delta) 先返回 AtomicInteger 当前的值,然后将其值加上指定的 delta。原子性操作。
AtomicInteger atomicInteger = new AtomicInteger(5);
int oldValue = atomicInteger.getAndAdd(3);
System.out.println("Old value: " + oldValue);
System.out.println("New value: " + atomicInteger.get());
  1. boolean compareAndSet(int expect, int update) 如果 AtomicInteger 当前的值等于 expect,则将其值设置为 update,并返回 true;否则返回 false。这是一个原子操作,它是 AtomicInteger 实现一些复杂操作的基础,例如乐观锁机制。
AtomicInteger atomicInteger = new AtomicInteger(5);
boolean success = atomicInteger.compareAndSet(5, 10);
System.out.println("Compare and set result: " + success);
System.out.println("New value: " + atomicInteger.get());
  1. int getAndUpdate(IntUnaryOperator updateFunction) 先返回 AtomicInteger 当前的值,然后根据 updateFunction 对其值进行更新。updateFunction 是一个 IntUnaryOperator 函数式接口,它接受一个 int 类型的参数并返回一个 int 类型的结果。
AtomicInteger atomicInteger = new AtomicInteger(5);
int oldValue = atomicInteger.getAndUpdate(i -> i * 2);
System.out.println("Old value: " + oldValue);
System.out.println("New value: " + atomicInteger.get());
  1. int updateAndGet(IntUnaryOperator updateFunction) 先根据 updateFunctionAtomicInteger 的值进行更新,然后返回更新后的值。
AtomicInteger atomicInteger = new AtomicInteger(5);
int result = atomicInteger.updateAndGet(i -> i * 2);
System.out.println("Updated value: " + result);
  1. int accumulateAndGet(int x, IntBinaryOperator accumulatorFunction)AtomicInteger 当前的值与 x 按照 accumulatorFunction 进行计算,然后返回计算结果。accumulatorFunction 是一个 IntBinaryOperator 函数式接口,它接受两个 int 类型的参数并返回一个 int 类型的结果。
AtomicInteger atomicInteger = new AtomicInteger(5);
int result = atomicInteger.accumulateAndGet(3, (i, j) -> i + j);
System.out.println("Accumulated value: " + result);
  1. int getAndAccumulate(int x, IntBinaryOperator accumulatorFunction) 先返回 AtomicInteger 当前的值,然后将其值与 x 按照 accumulatorFunction 进行计算并更新。
AtomicInteger atomicInteger = new AtomicInteger(5);
int oldValue = atomicInteger.getAndAccumulate(3, (i, j) -> i + j);
System.out.println("Old value: " + oldValue);
System.out.println("New value: " + atomicInteger.get());

四、AtomicInteger 在多线程环境中的应用

  1. 计数器场景 回到前面提到的多线程计数问题,使用 AtomicInteger 可以轻松解决。
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounterExample {
    private static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                count.incrementAndGet();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                count.incrementAndGet();
            }
        });

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println("Final count: " + count.get());
    }
}

在这个例子中,AtomicIntegerincrementAndGet 方法保证了每次自增操作的原子性,无论有多少个线程同时执行,最终的 count 值都能正确地达到 20000。

  1. 实现乐观锁 乐观锁机制在多线程编程中常用于提高并发性能。AtomicIntegercompareAndSet 方法可以用于实现简单的乐观锁。
import java.util.concurrent.atomic.AtomicInteger;

public class OptimisticLockExample {
    private static AtomicInteger version = new AtomicInteger(0);
    private static int data = 0;

    public static void main(String[] args) {
        // 线程 1 尝试更新数据
        Thread thread1 = new Thread(() -> {
            int expectedVersion = version.get();
            // 模拟一些业务操作
            int newData = data + 1;
            if (version.compareAndSet(expectedVersion, expectedVersion + 1)) {
                data = newData;
                System.out.println("Thread 1 updated data: " + data);
            } else {
                System.out.println("Thread 1 failed to update data due to version change");
            }
        });

        // 线程 2 尝试更新数据
        Thread thread2 = new Thread(() -> {
            int expectedVersion = version.get();
            // 模拟一些业务操作
            int newData = data + 2;
            if (version.compareAndSet(expectedVersion, expectedVersion + 1)) {
                data = newData;
                System.out.println("Thread 2 updated data: " + data);
            } else {
                System.out.println("Thread 2 failed to update data due to version change");
            }
        });

        thread1.start();
        thread2.start();
    }
}

在这个例子中,version 作为版本号,每个线程在更新数据前先获取当前版本号,然后在更新数据时使用 compareAndSet 方法检查版本号是否发生变化。如果版本号没有变化,则更新数据并递增版本号;否则,说明数据已经被其他线程修改,当前线程更新失败。

五、AtomicInteger 与 synchronized 的比较

  1. 性能方面
    • AtomicInteger:基于硬件层面的原子操作指令,在无竞争或低竞争的情况下,性能通常优于 synchronized。因为 AtomicInteger 不需要像 synchronized 那样获取锁,从而减少了线程上下文切换的开销。例如,在简单的计数器场景下,AtomicInteger 的自增操作可以直接利用硬件原子指令,而 synchronized 需要获取锁,会导致线程阻塞和上下文切换。
    • synchronized:在高竞争环境下,synchronized 可能会因为频繁的锁竞争导致性能下降。但是,当操作涉及到复杂的业务逻辑,需要保证一系列操作的原子性时,synchronized 可以通过同步块来包裹多个操作,确保原子性。而 AtomicInteger 只能保证单个操作的原子性,如果要实现多个 AtomicInteger 操作的原子性,需要额外的机制。
  2. 适用场景
    • AtomicInteger:适用于对单个整数进行原子操作的场景,如计数器、版本号等。它提供了丰富的原子操作方法,可以满足基本的数值计算需求。
    • synchronized:适用于需要保证多个操作原子性,或者需要对对象进行整体同步的场景。例如,在一个方法中需要对多个变量进行操作,并且这些操作必须是原子的,此时使用 synchronized 同步块会更加合适。

六、AtomicInteger 的实现原理

AtomicInteger 的实现主要依赖于 sun.misc.Unsafe 类的一些方法。Unsafe 类提供了直接操作内存和硬件的能力,通过它可以实现高效的原子操作。

AtomicInteger 内部使用一个 volatile int value 来存储实际的值。volatile 关键字保证了变量的可见性,即当一个线程修改了 value 的值,其他线程能够立即看到这个变化。

incrementAndGet 方法为例,其实现代码大致如下:

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

这里的 unsafesun.misc.Unsafe 的实例,valueOffsetvalue 字段在内存中的偏移量。unsafe.getAndAddInt 方法是一个本地方法,它利用 CPU 的原子操作指令(如 CAS - Compare And Swap)来实现原子的加法操作。CAS 操作会比较内存中的值和预期值,如果相等则将内存中的值更新为新值。这种方式避免了传统锁机制带来的线程阻塞和上下文切换开销,从而提高了并发性能。

七、注意事项

  1. 数据范围 AtomicInteger 是针对整数类型的原子操作类,它的取值范围与 int 类型一致,即 -21474836482147483647。如果需要处理更大范围的数值,可以考虑使用 AtomicLong
  2. 与其他并发工具的配合使用 在复杂的并发场景中,AtomicInteger 通常需要与其他并发工具(如 ConcurrentHashMapCountDownLatch 等)配合使用。例如,在一个分布式系统中,可能需要使用 AtomicInteger 来记录某个节点的任务执行次数,同时使用 CountDownLatch 来协调多个任务的执行顺序。
  3. 异常处理 AtomicInteger 的大部分方法不会抛出异常,但在使用 Unsafe 相关方法时,如果内存偏移量计算错误等情况,可能会导致 IllegalArgumentException 等异常。因此,在使用底层方法时需要谨慎处理。

通过对 AtomicInteger 的深入了解,我们可以在多线程编程中更加灵活和高效地处理整数类型的原子操作,提高程序的并发性能和数据一致性。无论是简单的计数器场景,还是复杂的乐观锁实现,AtomicInteger 都为我们提供了强大的工具。在实际应用中,需要根据具体的业务需求和并发场景,合理选择使用 AtomicInteger 或其他并发工具,以达到最佳的性能和稳定性。