唐山市网站建设_网站建设公司_Oracle_seo优化
2026/1/21 16:06:25 网站建设 项目流程

知识点 11:并发编程 —— 原子类与 CAS 原理

1. 核心理论:什么是原子操作?

在并发编程中,原子操作指的是一个不会被线程调度机制中断的操作。这种操作一旦开始,就一直运行到结束,中间不会有任何上下文切换。我们之前讨论过,count++ 不是一个原子操作,它包含“读-改-写”三个步骤,因此在多线程环境下是不安全的。

为了解决这个问题,除了使用锁(悲观锁)之外,Java 还提供了一种更高效的“无锁”解决方案——原子类

原子类位于 java.util.concurrent.atomic 包下,例如 AtomicInteger, AtomicLong, AtomicBoolean 等。它们通过一种名为 CAS (Compare-And-Swap) 的机制,以一种乐观的方式保证了复合操作的原子性。


2. 深度剖析:悲观锁 vs 乐观锁

在并发控制中,根据对冲突的“态度”不同,我们可以将锁分为两大类:悲观锁和乐观锁。

2.1 悲观锁 (Pessimistic Locking)

  • 核心思想: 非常悲观,它总是假设最坏的情况,即每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
  • 典型实现: Java 中的 synchronized 关键字和 ReentrantLock 等独占锁,都是悲观锁的实现。它们在操作数据之前,会先获取锁,确保在整个操作过程中,只有一个线程能访问数据。
  • 适用场景: 适合写多读少的场景,即冲突发生的概率很高。如果冲突频繁,使用悲观锁可以避免乐观锁大量的重试操作,反而能提高性能。

2.2 乐观锁 (Optimistic Locking)

  • 核心思想: 非常乐观,它总是假设最好的情况,即每次去拿数据的时候都认为别人不会修改。所以它不会上锁,而是在更新的时候会判断一下在此期间别人有没有去更新这个数据。
  • 典型实现: CAS (Compare-And-Swap) 机制就是乐观锁的典型实现。它在更新前,会先比较当前内存中的值与自己之前取到的值是否一致。如果一致,就执行更新;如果不一致,就认为有冲突,然后进行重试(自旋),直到成功为止。
  • 适用场景: 适合读多写少的场景,即冲突发生的概率很低。因为不上锁,省去了锁的开销,可以获得更高的吞吐量。

3. CAS (Compare-And-Swap) 原理

CAS 是实现乐观锁和原子类的核心技术,是现代 CPU 广泛支持的一种原子指令

  • CAS 指令包含三个操作数

    1. 内存位置 V (要更新的变量的内存地址)
    2. 预期原值 A (我们认为这个变量现在应该是什么值)
    3. 新值 B (如果变量的值和我们预期的 A 一样,就把它更新成 B)
  • 执行过程: 当执行 CAS 指令时,CPU 会原子地完成以下判断和操作:当且仅当内存位置 V 的值等于预期原值 A 时,CPU 才会将该位置的值更新为新值 B。否则,它什么也不做。无论成功与否,它都会返回操作前的旧值。

AtomicInteger.getAndIncrement() 的工作流程 (即 i++ 的原子版)**:

`AtomicInteger` 能保证复合操作的原子性,其秘诀就在于 **“CAS + 循环重试(自旋)”**。1.  在一个**无限循环**(`do-while` 循环,也常被称为**自旋 (Spinning)**)中进行。
2.  **读取**当前 `AtomicInteger` 的 `value` 值(这是一个 `volatile` 变量),我们称之为 `current`。这个值就是我们的“预期原值 A”。
3.  计算出“新值 B”,即 `current + 1`。
4.  调用 `compareAndSet(current, current + 1)` 方法,这个方法会执行底层的 CAS 原子指令,尝试将内存中的值从 `current` 更新为 `current + 1`。
5.  **检查 CAS 结果**:`compareAndSet` 会返回 `true` 或 `false`。-   如果返回 `true`(成功了),意味着在“读-改-写”的瞬间,没有其他线程修改过这个值。循环结束,操作完成。-   如果返回 `false`(失败了),意味着在我们准备写入时,有其他线程已经抢先修改了值。循环**不会结束**,而是会**回到第 2 步**,重新读取最新的值,然后再次尝试 CAS。这个“失败后重试”的过程就是**自旋**。

3.1 CAS 的潜在问题

  1. ABA 问题:

    • 问题描述: 这是 CAS 的一个经典漏洞。如果一个变量的值从 A 变成了 B,然后又变回了 A。当一个线程执行 CAS 时,它会检查发现当前值是 A,与预期值 A 相符,于是执行更新。但它不知道这个值其实中间被改动过。
    • 解决方案: JUC 包提供了 AtomicStampedReference 类来解决这个问题。它通过为每个值关联一个额外的“版本号”(stamp),CAS 操作不仅要检查值是否相等,还要检查版本号是否相等,从而避免了 ABA 问题。
  2. 自旋时间长,开销大: 其实就是 CAS 的开销问题

    • 问题描述: 如果锁的竞争一直很激烈,会导致线程反复地尝试 CAS 操作但一直失败。这个“自旋”的过程会持续占用 CPU,造成大量的计算资源浪费。
    • 结论: 在高竞争的环境下,synchronized 这种能让线程进入阻塞等待状态的“重量级锁”,其性能表现反而可能会优于 CAS 这种需要不断自旋的乐观锁。

4. 生活中的例子与代码示例

  • 生活比喻: 想象你在拍卖会上竞拍一件物品。

    • 共享变量: 当前的最高出价
    • 你的操作 (increment): 你想在当前最高出价的基础上加 100 元。
    • CAS 流程:
      1. 你看到当前的最高出价是 1000 元(读取预期原值 A)。
      2. 你决定出价 1100 元(计算新值 B)。
      3. 你举牌喊价(执行 CAS)。在喊价的瞬间,拍卖师会原子地做一次判断:
        • 成功: 如果在你喊价之前,最高出价仍然是 1000 元,那么你的出价成功,最高价更新为 1100 元。
        • 失败: 如果在你举牌的瞬间,另一个人(其他线程)已经抢先喊出了 1050 元,那么拍卖师会认为你的出价(基于 1000 元)无效。你必须重新听一下现在的最高价是 1050 元,然后在这个基础上再次出价(自旋重试)。
  • 核心代码示例: 我们用原子类来改造之前的 Counter 实验。

package com.study.concurrency;import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;// 使用 AtomicInteger 实现的线程安全的计数器
class AtomicCounter {// 1. 使用 AtomicInteger 作为计数器private AtomicInteger count = new AtomicInteger(0);public void increment() {// 2. getAndIncrement() 方法本身就是原子的,它内部封装了 CAS 循环count.getAndIncrement();}public int getCount() {return count.get();}
}public class AtomicTest {public static void main(String[] args) throws InterruptedException {final AtomicCounter counter = new AtomicCounter();ExecutorService threadPool = Executors.newFixedThreadPool(10);Runnable task = () -> {for (int i = 0; i < 1000; i++) {counter.increment();}};for (int i = 0; i < 10; i++) {threadPool.submit(task);}threadPool.shutdown();while (!threadPool.awaitTermination(1, TimeUnit.SECONDS)) {System.out.println("线程池仍在运行...");}System.out.println("\n所有任务执行完毕。");System.out.println("AtomicCounter 的最终值为: " + counter.getCount());System.out.println("预期值应为: 10000");}
}

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询