原子类-面试

发布于:2025-09-13 ⋅ 阅读:(19) ⋅ 点赞:(0)

         有一个同学问我什么是JUC:JUC 是 java.util.concurrent 包的缩写。这个包是 Java 专门为处理多线程并发编程而提供的一个“工具箱”。

原子类

定义

        原子类(Atomic Classes) 是 Java 在 java.util.concurrent.atomic 包下提供的一组工具类,用于在多线程环境下,无需使用锁(如 synchronized)即可实现单个变量操作的原子性、线程安全性和内存可见性

分类

JUC包下的原子类

1. 基本类型原子类

用于通过 CAS 操作原子性地更新基本类型。

  • AtomicInteger: 原子更新整型。(最常用)

  • AtomicLong: 原子更新长整型。

  • AtomicBoolean: 原子更新布尔类型。

2. 数组类型原子类

用于原子性地更新数组里的某个元素。

  • AtomicIntegerArray: 原子更新整型数组里的元素。

  • AtomicLongArray: 原子更新长整型数组里的元素。

  • AtomicReferenceArray: 原子更新引用类型数组里的元素。

3. 引用类型原子类
  • AtomicReference: 原子更新引用类型。可以用于实现诸如自旋锁、缓存等数据结构。

  • AtomicStampedReference: 原子更新引用类型,内部通过一个 int 类型的版本号(Stamp) 来解决 CAS 操作中的 ABA 问题

  • AtomicMarkableReference: 原子更新引用类型,内部通过一个 boolean 类型的标记来表示该引用是否被修改过。它是 AtomicStampedReference 的一个简化版,不关心修改次数,只关心是否被修改过。

4. 字段更新器(Updater)

以一种线程安全的方式原子性地更新某个类的特定 volatile 字段。这些字段不需要是 AtomicXXX 对象,可以是普通的成员变量。使用相对较少,主要用于性能极致的场景。

  • AtomicIntegerFieldUpdater

  • AtomicLongFieldUpdater

  • AtomicReferenceFieldUpdater  

通俗理解

        原子类封装了一个 volatile 变量,并通过 硬件级别的 CAS (Compare-And-Swap) 指令 来保证对该变量进行“读-改-写”操作的原子性。

CAS

        CAS 的全称是 Compare-And-Swap(比较并交换)。它是一种无锁的乐观的原子算法。

        它的核心思想:“我认为值应该是A,如果是的话,就把它改成B;如果不是A(说明被别人改过了),那我就不修改了,然后告诉我现在的值是多少。

核心原理

CAS 操作需要三个操作数:

  1. V: 要读写的内存位置(例如,一个变量的地址)

  2. A: 预期的原始值(你认为这个内存位置当前的值应该是什么)

  3. B: 想要写入的新值

算法流程:

  1. 检查内存位置 V 的值是否与预期值 A 相等。

  2. 如果相等,处理器会自动将该位置的值更新为新值 B

  3. 如果不相等,说明有其他线程修改了 V,本次操作不做任何修改(或者可以选择重试)。

  4. 无论是否修改成功,都会返回 V 的当前实际值

  • 优点

    • 高性能:在没有激烈竞争的情况下,开销远小于悲观锁(因为它避免了线程挂起和上下文切换)。

    • 无阻塞:线程永远不会被挂起,如果失败可以立即重试或做其他操作。

  • 缺点

    • ABA 问题:如果一个值原来是 A,变成了 B,后来又变回了 A。CAS 在“比一比”时会发现它没变,于是操作成功,但其实它中间已经发生过变化。(可以用AtomicStampedReference加版本号解决)。

    • 循环时间长开销大:如果竞争非常激烈,CAS 一直失败,线程会不停重试,消耗 CPU 资源。

    • 只能保证一个变量的原子性:只能对一个变量进行原子操作,不能保证多个变量共同操作的原子性(但可以合并成一个对象再用 AtomicReference 来保证)。

理解示例

操作i++操作

1、现成不安全时:

public class UnsafeCounter {
    private int count = 0;
    public void increment() {
        count++; // 这不是原子操作!
    }
    public int getCount() {
        return count;
    }
}

2、线程安全、能保证原子性,但是性能损耗

public class SynchronizedCounter {
    private int count = 0;
    public synchronized void increment() { // 加锁,保证原子性
        count++;
    }
    public synchronized int getCount() { // 读操作也需要加锁保证可见性
        return count;
    }
}

3、乐观锁/CAS

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounter {
    // 定义:一个 AtomicInteger 类型的原子变量
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        // 底层使用 CAS 保证原子性
        count.incrementAndGet(); // 相当于 ++count
    }

    public int getCount() {
        // 直接返回,因为内部 volatile 保证了可见性
        return count.get();
    }
}

原子类 = volatile + CAS

核心是它通过 volatile 保证了可见性,通过 CAS 操作保证了原子性,两者结合最终实现了无锁化的线程安全。

底层硬件实现

CAS 并非通过软件(如Java代码)实现,它的原子性依赖于计算机硬件。

主要实现方式有两种:

  1. 总线锁:早期处理器通过在总线上发出一个 LOCK# 信号,锁定整个内存系统,阻止其他处理器或核心访问内存。这种方式锁的粒度太粗,性能开销大。

  2. 缓存锁(MESI协议):现代处理器更常用的方式。它不锁总线,而是基于缓存一致性协议(如 Intel 的 MESI 协议)来保证原子性。

    • MESI 定义了缓存行(Cache Line)的四种状态:Modified(修改)、Exclusive(独占)、Shared(共享)、Invalid(无效)。

    • 当某个CPU核心要执行CAS操作时,它会锁定自己缓存中的对应缓存行

    • 如果它发现缓存行的状态表明数据是独占的(E),并且值等于期望值A,它就可以安全地更新缓存行为新值B,并将状态置为M。

    • 如果它发现数据是共享的(S),或者值不等于A,说明有其他核心也在使用这个数据,本次CAS操作就会失败。

核心要点: 无论哪种方式,最终都通过CPU提供的一条机器指令(如 x86 架构下的 CMPXCHG 指令)来完成这个比较和交换的操作。JVM 只是调用了这条指令的包装。

悲观锁和乐观锁对比

特性 悲观锁(sch) 乐观锁
基本思想 “悲观”地认为并发冲突一定会发生。因此,在操作数据之前,会先加锁,确保在整个操作过程中,数据不会被其他线程修改。 “乐观”地认为并发冲突很少发生。因此,不会直接加锁,而是在提交更新时,才检查数据在此期间是否被其他线程修改过。
类比 就像独占写文章。你认为只要自己离开座位,别人就一定会来改动你的文章。所以你在写的时候就把门锁上,不让任何人进来,直到你写完才开门。 就像协作编辑文档(如Google Docs)。你和同事都可以同时编辑。你们各自保存时,系统会检查自你打开文档后是否有其他人的修改。如果有,它会提示你冲突并让你解决。
实现机制 依靠数据库或语言的原生锁机制,如:行锁、表锁、读写锁、synchronized关键字等。 通常通过数据版本号(Version) 或时间戳(Timestamp) 实现。
工作流程 1. 开始事务
2. 申请并获得锁
3. 进行业务操作
4. 提交事务并释放锁
1. 读取数据,并记录版本号 V1
2. 进行业务操作(不加锁)
3. 提交更新时,检查当前版本号是否仍为 V1
- 如果是:提交成功,并更新版本号(如 V1+1)
- 如果不是:提交失败,进行重试或抛出异常