likes
comments
collection
share

Java并发编程面试6:原子变量: AtomicInteger, AtomicLong和AtomicReference

作者站长头像
站长
· 阅读数 22

引言

在现代软件开发中,多线程和并发已经成为提高应用性能和效率的关键技术。然而,正确地管理并发环境中的共享资源是一项挑战,尤其是在涉及到多个线程同时读写共享变量时。为了解决这一问题,Java 提供了一套原子类,它们位于 java.util.concurrent.atomic 包中。这些原子类利用底层硬件的 CAS(Compare-And-Swap)操作,为开发者提供了一种无锁的线程安全机制,使得并发编程变得更加简单和高效。本文将深入探讨其中的三个常用原子类:AtomicInteger, AtomicLong, 和 AtomicReference,并通过具体的示例展示它们在实际并发场景中的应用。

Unsafe

Unsafe 是 Java 中 sun.misc 包下的一个类,它提供了一组用于执行低级别、不安全操作的方法,通常这些操作是由 Java 的标准库隐藏的。 由于 Unsafe 类的方法可能会破坏 Java 的内存安全保证,因此它不是公开的 API,而是被 Java 标准库内部使用。

实现原理:

Unsafe 类能够直接操作内存,它的功能包括:

  1. 内存管理: Unsafe 提供了分配、释放、访问对象的低级方法,允许开发者直接对 Java 堆外内存进行操作。

  2. 字段操作: Unsafe 可以获取和设置对象的字段值,甚至是私有的或者被标记为 final 的字段,而不需要通过正常的访问控制检查。

  3. 数组操作: Unsafe 提供了访问数组元素的方法,可以直接通过计算偏移地址来访问,这是一种比标准 Java 访问方式更底层的方法。

  4. CAS 操作: Unsafe 提供了各种 CAS(Compare-And-Swap)操作,它们是实现无锁数据结构的基础。

  5. 内存屏障: Unsafe 提供了插入内存屏障的方法,这对于开发并发算法和数据结构非常重要。

与 AtomicInteger, AtomicLong 和 AtomicReference 的关系:

AtomicInteger, AtomicLong, 和 AtomicReference 都是 java.util.concurrent.atomic 包下的类,它们提供了对单个变量的原子操作。这些类的实现底层依赖于 Unsafe 类提供的 CAS 操作。

  1. AtomicInteger: AtomicInteger 提供了对整型变量的原子操作。它使用 Unsafe 类中的 compareAndSwapInt 方法来实现原子更新。

  2. AtomicLong: AtomicLongAtomicInteger 类似,但它提供了对长整型变量的原子操作。它使用 Unsafe 类中的 compareAndSwapLong 方法来实现原子更新。

  3. AtomicReference: AtomicReference 提供了对对象引用的原子操作。它使用 Unsafe 类中的 compareAndSwapObject 方法来实现原子更新。

所有这些原子类都使用 Unsafe 类的 CAS 方法来保证对变量的原子操作。它们封装了 Unsafe 的使用,提供了一个安全且易于使用的 API,使得开发者可以在高并发环境下安全地执行非阻塞同步。

总结:

Unsafe 类是在 Java 标准库中实现低级别并发原语的基础, 而 AtomicInteger, AtomicLong, 和 AtomicReference 这些原子类则建立在这些原语之上,提供了易于使用的并发原子操作。 虽然 Unsafe 提供了强大的功能,但它的使用需要特别小心,因为不当的使用可能会导致程序出现难以发现的错误。 在大多数情况下,推荐使用标准库提供的高级并发工具,而不是直接使用 Unsafe

AtomicInteger

AtomicInteger 是 Java 中 java.util.concurrent.atomic 包下的一个类,提供了一种在单个变量上进行原子操作的方式。它 利用底层硬件的原子指令来保证对整数值的操作是原子的,即这些操作在执行的过程中不会被其他线程中断。

实现原理:

  1. CAS(Compare-And-Swap): AtomicInteger 主要依靠 CAS 操作实现原子性。CAS 是一种无锁的非阻塞算法,它包含三个参数:内存位置(V)、预期原值(A)和新值(B)。CAS 操作的执行步骤是:当且仅当内存位置的值与预期原值相匹配时,将内存位置的值修改为新值,否则不做任何操作。在 Java 中,这是通过调用 Unsafe 类中的 native 方法实现的。

  2. volatile: AtomicInteger 内部的 int 值被声明为 volatile,这保证了该变量的可见性,即当一个线程修改了这个变量的值时,其他线程能够立即看到这个新值。

优点:

  1. 线程安全: 所有操作都是原子的,保证了在多线程环境下的线程安全。

  2. 性能: 相比于使用 synchronized 关键字或者 Lock 实现的锁机制,AtomicInteger 基于 CAS 的非阻塞算法在高并发环境下通常能提供更好的性能。

  3. 无锁机制: AtomicInteger 通过 CAS 实现无锁机制,避免了线程阻塞和唤醒的开销。

  4. 易用性: 提供了一组丰富的原子操作方法,如 getAndIncrementincrementAndGetaddAndGet 等,使用起来简单方便。

缺点:

  1. ABA 问题: CAS 操作可能会遇到 ABA 问题,即一个值原来是 A,变成了 B,又变回了 A,CAS 会认为这个值没有变过。在 AtomicInteger 的场景下, 这通常不是问题,因为关心的是数值的最终状态,而不是中间状态。

  2. 自旋开销: 当多个线程同时尝试更新同一个 AtomicInteger 时,如果 CAS 操作失败,线程会不断重试(自旋),这可能会导致 CPU 的过度消耗。

  3. 有限的应用场景: AtomicInteger 只能保证单个变量的原子操作,对于复杂的同步需求或者多个变量的原子性操作,它就无能为力了。

  4. 伪共享(False Sharing): 如果多个 AtomicInteger 实例紧密排列在一起,它们可能会位于同一个缓存行上,导致伪共享问题。当一个线程修改一个 AtomicInteger 实例时,可能会无意中影响到位于同一缓存行上的其他实例。

AtomicInteger 使用场景和并发中的使用:

使用场景:

  1. 计数器: 用于记录事件发生的次数,如服务器接收到的请求数。
  2. 序列号生成器: 生成唯一序列号。
  3. 状态标记: 标记某个过程的状态,如任务是否完成。

并发中的使用:

  • 使用 incrementAndGet()getAndIncrement() 方法实现原子性的递增操作。
  • 使用 decrementAndGet()getAndDecrement() 方法实现原子性的递减操作。
  • 使用 compareAndSet(expectedValue, newValue) 方法在预期值未改变的情况下更新值。

总的来说,AtomicInteger 提供了一种高效的线程安全的整数操作方式,适用于计数器或者累加器等简单原子操作的场景。在更复杂的同步需求中,可能需要考虑其他同步机制。

AtomicLong

AtomicLong 是 Java 中 java.util.concurrent.atomic 包提供的一个类,它允许进行原子操作,特别是针对 long 类型的值。这个类的设计目的是在多线程环境中,提供一种线程安全的方式来操作单个 long 值,无需使用 synchronized 关键字。

实现原理:

AtomicLong 的原子性保证主要基于 CAS(Compare-And-Swap)操作。CAS 是一种硬件对并发操作的支持,它涉及三个操作数:

  • 一个内存位置(V,代表变量的地址)
  • 预期原值(A)
  • 新值(B)

只有当内存位置的当前值与预期原值(A)相匹配时,处理器才会自动将该位置值更新为新值(B)。这个更新是原子的,即中间不会被其他线程打断。

在 Java 中,AtomicLong 利用 sun.misc.Unsafe 类中的 compareAndSwapLong 方法来实现 CAS。由于 long 类型的值可能跨越多个内存字,所以需要硬件级别的支持来保证操作的原子性。

优点:

  1. 线程安全AtomicLong 提供了一种无锁的机制来保证对 long 值的操作在线程之间是安全的。

  2. 高性能:在高并发场景下,AtomicLong 相比于使用锁的方式(如 synchronizedLock 接口)通常能提供更好的性能,因为它减少了上下文切换和线程调度的开销。

  3. 无阻塞AtomicLong 的操作通常不会导致线程阻塞,因为它们不涉及锁等待。

  4. 原子操作集合AtomicLong 提供了一系列原子操作,如 getAndIncrementincrementAndGetgetAndSetcompareAndSet 等,这些都是单步且线程安全的。

缺点:

  1. 自旋开销:在高争用的情况下,如果多个线程频繁地尝试更新同一个 AtomicLong,它们可能会进行多次失败的 CAS 尝试,这被称为自旋,会增加 CPU 的消耗。

  2. 限制性AtomicLong 只能对单个 long 值进行原子操作。如果需要对多个变量或者复杂的数据结构进行原子操作,AtomicLong 就无能为力了。

  3. ABA 问题:尽管在 AtomicLong 的上下文中 ABA 问题不太常见,但 CAS 操作本身可能受到 ABA 问题的影响,即在变量值从 A 变为 B 再变回 A 的过程中,CAS 会认为变量没有被修改过。

  4. 伪共享(False Sharing):如果多个 AtomicLong 实例紧邻存放,它们可能位于同一缓存行中。当一个线程修改一个实例时,可能会导致其他实例所在的缓存行失效,这会降低性能。

总结来说,AtomicLong 是一个用于线程安全操作 long 值的工具,它在多线程环境下提供了非阻塞的高性能原子操作。然而,它的使用也受限于只能操作单个变量,并且在某些高争用场景下可能会导致性能问题。

AtomicLong 使用场景和并发中的使用:

使用场景:

  1. 大数值计数器: 当需要计数超过 Integer.MAX_VALUE 时使用。
  2. 统计累加值: 如累计文件大小或者时间。
  3. 唯一 ID 生成: 用于生成数据库记录的唯一 ID。

并发中的使用:

  • 类似于 AtomicInteger,提供了 incrementAndGet()getAndIncrement()decrementAndGet()getAndDecrement() 等方法。
  • 使用 addAndGet(delta)getAndAdd(delta) 方法实现原子性的累加操作。
  • 使用 compareAndSet(expectedValue, newValue) 方法进行条件更新。

AtomicReference

AtomicReference 是 Java 中 java.util.concurrent.atomic 包提供的一个类,它提供了对引用类型的对象进行原子操作的能力。通过这个类,可以安全地在多线程环境中更新对象引用,而无需使用同步机制,如 synchronized 关键字或显式锁。

实现原理:

AtomicReference 的实现依赖于 CAS(Compare-And-Swap)操作。CAS 是一种底层原子操作,它涉及三个操作数:

  • 一个内存位置(V,代表变量的地址)
  • 预期原值(A)
  • 新值(B)

CAS 操作会自动检查内存位置 V 上的值是否与预期原值 A 相同。如果是,它会将内存位置 V 上的值更新为新值 B。这个过程是原子的,即不可分割的,保证了在多线程环境中的线程安全。

在 Java 中,AtomicReference 利用 sun.misc.Unsafe 类中的 compareAndSwapObject 方法来实现 CAS。Unsafe 类提供了底层的、不安全的操作,但 AtomicReference 对这些操作进行了封装,使得它们可以安全地用于并发编程。

优点:

  1. 线程安全AtomicReference 保证了引用类型的原子更新,使得多线程环境下的编程更加安全。

  2. 无锁机制:由于基于 CAS,AtomicReference 提供了无锁的原子操作,避免了传统同步机制可能引入的死锁或线程饥饿问题。

  3. 性能:在不涉及高争用的情况下,AtomicReference 的性能通常优于使用锁的同步机制,因为它减少了线程上下文切换的开销。

  4. 简单 APIAtomicReference 提供了易于使用的 API,如 getsetgetAndSetcompareAndSet 等,使得原子操作变得简单直接。

缺点:

  1. 自旋与争用:在竞争激烈的环境中,多个线程尝试同时更新同一个 AtomicReference 可能会导致频繁的自旋和重试,这可能会增加 CPU 资源的消耗。

  2. 有限的操作集AtomicReference 只能保证单个引用的原子性。对于需要同时更新多个相关联变量的复杂操作,它无法提供原子性保证。

  3. ABA 问题AtomicReference 可能会遇到 ABA 问题,即一个引用原先指向 A 对象,后来指向了 B 对象,最终又回到了 A 对象。在这种情况下,CAS 操作会认为引用没有改变,尽管实际上它在中间变化过。这可能会在某些算法中造成问题。

  4. 内存占用:每个 AtomicReference 对象都会比普通引用多占用一些内存,因为它们需要存储额外的原子状态信息。

  5. 伪共享(False Sharing):和其他原子类一样,如果多个 AtomicReference 实例紧邻存放,它们可能位于同一缓存行中。这可能导致缓存行频繁失效,降低性能。

AtomicReference 使用场景和并发中的使用:

使用场景:

  1. 共享对象的引用更新: 当需要在多个线程间共享并更新对象引用时。
  2. 实现无锁数据结构: 如无锁的链表、栈、队列等。
  3. 实现原子性的快照: 通过原子更新来保证取到对象引用的一致性视图。

并发中的使用:

  • 使用 get() 方法获取当前引用。
  • 使用 set(newValue) 方法更新引用,但这不是原子操作。
  • 使用 getAndSet(newValue) 方法原子性地更新引用并返回旧值。
  • 使用 compareAndSet(expectedReference, newReference) 方法在引用未改变的情况下原子性地更新。

总结来说,AtomicReference 提供了一种线程安全的方式来更新对象引用,特别适合于实现无锁的并发数据结构。然而,在高争用的场景或需要原子更新多个变量的复杂场景中,其性能和适用性可能会受到限制。

注意事项:

  1. 正确理解原子性: 这些原子类只能保证单个变量的原子操作,对于多个变量的复合操作,需要其他同步机制。
  2. 避免长时间自旋: 在高争用环境下,过度的自旋可能导致性能问题,考虑使用其他同步机制。
  3. 注意 ABA 问题: 在某些算法中,需要注意 CAS 操作可能遇到的 ABA 问题。
  4. 避免伪共享: 尽量避免将多个原子变量放置在相邻的内存位置,以减少缓存行的无效化。
  5. 使用悲观锁作为备选方案: 如果原子类无法满足需求或性能不佳,可以考虑使用 synchronizedReentrantLock 等悲观锁方案。

测试用例

package com.dereksmart.crawling.core;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
/**
 * @Author derek_smart
 * @Date 2024/8/13 8:15
 * @Description 演示如何在并发环境中使用 `AtomicInteger`, `AtomicLong`, 和 `AtomicReference` 类
 */
public class AtomicTest {
    private AtomicInteger concurrentVisits = new AtomicInteger(0);
    private AtomicLong sequenceNumber = new AtomicLong(0);
    private AtomicReference<Config> config = new AtomicReference<>(new Config("localhost", 8080));
//`Config` 是一个静态内部类,表示配置信息,包含主机名和端口号
    static class Config {
        private final String host;
        private final int port;

        public Config(String host, int port) {
            this.host = host;
            this.port = port;
        }

        @Override
        public String toString() {
            return host + ":" + port;
        }
    }

    /**
     * 每个任务都会增加 `concurrentVisits` 的计数,稍作等待,然后减少计数。这模拟了并发访问的场景。
     */
    public void testAtomicInteger() {
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        for (int i = 0; i < 100; i++) {
            executorService.submit(() -> {
                concurrentVisits.incrementAndGet();
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                concurrentVisits.decrementAndGet();
            });
        }

        executorService.shutdown();
        while (!executorService.isTerminated()) {
            // Wait for all tasks to finish
        }
        System.out.println("Final concurrent visits: " + concurrentVisits.get());
    }

    /**
     * 每个任务都会获取并打印一个唯一的序列号。
     */
    public void testAtomicLong() {
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        for (int i = 0; i < 100; i++) {
            executorService.submit(() -> {
                long seq = sequenceNumber.incrementAndGet();
                System.out.println("Generated sequence: " + seq);
            });
        }

        executorService.shutdown();
    }

    /**
     * 每个任务都会读取并打印当前的配置信息。
     */
    public void testAtomicReference() {
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        for (int i = 0; i < 100; i++) {
            executorService.submit(() -> {
                System.out.println("Current config: " + config.get());
            });
        }

        updateConfig("example.com", 9090);
        executorService.shutdown();
    }

    public void updateConfig(String newHost, int newPort) {
        config.set(new Config(newHost, newPort));
    }

    public static void main(String[] args) {
        AtomicTest atomicTest = new AtomicTest();

        atomicTest.testAtomicInteger();
        atomicTest.testAtomicLong();
        atomicTest.testAtomicReference();
    }
}

总结:

通过详细分析和实际示例,讨论了 AtomicInteger, AtomicLong, 和 AtomicReference 这三个 Java 原子类的使用方法和场景。了解到 AtomicIntegerAtomicLong 非常适合于实现计数器或序列生成器,而 AtomicReference 则能够帮助安全地处理对象引用的并发更新。

AtomicInteger 的示例中,展示了如何使用它来跟踪并发访问的数量,这在 web 服务器的请求计数等场景中非常有用。AtomicLong 的示例则演示了如何生成唯一的序列号,这对于数据库记录或任何需要唯一标识的应用场景都是必需的。最后,AtomicReference 的示例展示了如何原子性地更新配置信息,这在需要热更新配置而不中断服务的应用中非常实用。

虽然原子类提供了一种高效的并发解决方案,但使用它们时也需要注意一些问题。例如,过度的自旋可能导致 CPU 资源的浪费,在高争用环境下,可能需要考虑其他同步机制。此外,原子类只能保证单个变量的原子操作,如果需要原子性地更新多个相关联的变量,可能需要使用锁或其他并发工具。

总的来说,原子类是 Java 并发工具箱中的重要组件,它们为构建高效且稳健的并发应用程序提供了坚实的基础。理解并正确使用这些原子类,将有助于开发出更加可靠和响应迅速的多线程应用。

转载自:https://juejin.cn/post/7402204086430089266
评论
请登录