定西市网站建设_网站建设公司_版式布局_seo优化
2026/1/21 16:03:32 网站建设 项目流程

知识点 5.5:并发编程基石 —— AQS 与 Volatile

在深入了解各种锁和同步工具之前,必须先理解 JUC 框架的两个核心基石:volatile 关键字和 AQS 框架。


1. volatile 关键字:并发编程的“信号旗”

volatile 是一个 Java 变量修饰符,它非常轻量,但提供了两个至关重要的保证。

1.1 保证可见性 (Visibility)

  • 问题所在: 在多核 CPU 架构下,每个 CPU 都有自己的高速缓存(Cache)。当一个线程(运行在 CPU-1 上)修改了一个共享变量的值,它可能只是更新了自己的缓存,而没有立刻写回主内存。此时,另一个线程(运行在 CPU-2 上)读取这个变量时,可能会从自己的缓存里读到一个过时的、旧的值,导致数据不一致。

  • volatile 的作用: 当一个变量被声明为 volatile 后,JVM 会保证:

    1. 写操作:对这个变量的修改会立即被刷新到主内存中。
    2. 读操作:每次读取这个变量,都会强制从主内存中重新加载,而不是使用线程本地的缓存。
  • 生活比喻: volatile 变量就像一个公共的、唯一的电子公告牌。当一个线程更新了公告牌上的内容,其他所有线程都能立刻看到最新的内容,而不是看自己手里抄录的、可能已经过时的小纸条。

1.2 禁止指令重排序 (Instruction Reordering)

  • 问题所在: 为了提高性能,编译器和 CPU 可能会在不影响单线程最终结果的前提下,打乱代码的执行顺序。但在多线程环境下,这种重排序可能会导致意想不到的错误(例如著名的“双重检查锁定”单例模式失效问题)。

  • volatile 的作用: volatile 关键字会作为一个“内存屏障”(Memory Barrier)。当程序执行到 volatile 变量的读或写操作时,它前面的所有操作都必须已经完成,且结果对后续操作可见;它后面的所有操作也必须在它之后才能开始。这有效地阻止了指令重排序跨越这个屏障。

1.3 volatile 不能保证原子性

这是 volatile 最重要的一个局限性,也是面试高频陷阱。

  • 原子性: 指一个操作是不可分割、不可中断的。要么完全执行成功,要么完全不执行。

  • count++ 的问题: 这个操作看似一步,实际包含三步:“1. 读取 count 的值;2. 将值加 1;3. 将新值写回 count”。volatile 无法保证这三步作为一个整体的原子性。

演示代码:VolatileNoAtomicDemo.java

以下代码清晰地演示了 volatile 无法保证复合操作(如 ++)的原子性。期望结果是 100,000,但实际运行结果通常会小于此值,证明有自增操作丢失。


package com.study.concurrency;import java.util.concurrent.atomic.AtomicInteger; // 引入 AtomicInteger/*** 演示 volatile 不保证原子性的经典例子:多线程计数器* 运行结果:最终 count < 期望结果,证明 volatile 无法保证“++”操作的原子性*/
public class VolatileNoAtomicDemo {// 原始问题:使用 volatile 修饰共享变量,但 ++ 操作非原子private static volatile int count = 0; // 此处用 volatile 无法解决原子性问题// 用于解决原子性问题的方案一:使用 AtomicInteger// private static AtomicInteger atomicCount = new AtomicInteger(0);// 用于解决原子性问题的方案二:使用 synchronized 块// private static final Object lock = new Object();private static final int THREADS = 10;private static final int INCREMENTS_PER_THREAD = 10_000;public static void main(String[] args) throws InterruptedException {Thread[] threads = new Thread[THREADS];for (int i = 0; i < THREADS; i++) {threads[i] = new Thread(() -> {for (int j = 0; j < INCREMENTS_PER_THREAD; j++) {// 原始问题代码:count++; // 非原子操作:读-改-写三步// 方案一修正:使用 AtomicInteger// atomicCount.incrementAndGet(); // 原子操作// 方案二修正:使用 synchronized// synchronized (lock) { // 或者 synchronized (VolatileNoAtomicDemo.class)//     count++;// }}});threads[i].start();}for (Thread t : threads) {t.join();}System.out.println("期望结果 = " + (THREADS * INCREMENTS_PER_THREAD));// System.out.println("实际结果 (volatile) = " + count); // 如果使用 volatile int countSystem.out.println("实际结果 = " + count); // 或者使用 atomicCount.get()}
}

问题分析:为什么 volatile 会失效?

  • volatile 只能保证可见性有序性,即保证读取到 count 的值是最新的,并且阻止了指令重排序。

  • 但是,count++ 并非一个单一的原子操作。当多个线程同时进行“读取-修改-写入”这三个步骤时,即使它们都读取到了最新的值,也可能在修改和写入的间隙被其他线程抢占 CPU,导致更新丢失。例如:

    1. 线程A 读取 count (值为 100)。
    2. 线程B 读取 count (此时 count 仍是 100,因为线程A还没写回)。
    3. 线程A 将 count 计算为 101,并写入主内存。
    4. 线程B 将 count 计算为 101,并写入主内存。
    • 结果:count 最终是 101,而不是预期的 102。一个自增操作丢失。

如何修正这个问题来保证线程安全?
要保证 count++ 这样的复合操作的原子性,必须使用更强大的同步机制:

  1. 使用 synchronized 关键字

    • count++ 操作放入 synchronized 代码块中。synchronized 保证了同一时间只有一个线程可以进入该代码块,从而使整个“读-改-写”序列变为原子操作。
    // 示例:
    // synchronized (VolatileNoAtomicDemo.class) { // 或 synchronized (任意共享对象)
    //     count++;
    // }
    
    • synchronized 能够保证原子性、可见性、有序性。但它是一个重量级锁,在高并发竞争时性能开销较大。
  2. 使用 java.util.concurrent.atomic 包下的原子类 (推荐)

    • 对于简单的原子操作(如整数自增、布尔值设置等),Java 提供了 AtomicInteger, AtomicLong, AtomicBoolean 等原子类。
    • 这些类内部通过CAS (Compare-And-Swap) 等无锁机制来保证操作的原子性,性能通常比 synchronized 更好。
    // 示例:
    // private static AtomicInteger count = new AtomicInteger(0);
    // ...
    // count.incrementAndGet(); // 原子性地执行 count++
    
    • 结论volatile 适用于一写多读的场景,或者当变量的更新不依赖于其当前值时。对于 count++ 这样的复合操作,必须使用 synchronizedAtomicInteger 这样的原子类来保证线程安全。

2. AQS (AbstractQueuedSynchronizer):JUC 的龙骨

AQS 是 JUC 中绝大部分锁和同步工具的核心实现框架。它是一个抽象类,本身不是一个锁,而是用来构建锁和同步器的基础骨架。

2.1 AQS 的核心设计思想

AQS的核心思想是一种模板方法模式的应用。它提供了一个同步状态管理state)和线程排队(FIFO队列)的基础框架。开发者只需实现对state的获取和释放逻辑,而线程的排队、阻塞、唤醒等复杂操作都由AQS框架本身来完成。

2.2 两大核心数据结构

  1. volatile int state: 一个用 volatile 修饰的整型变量,用于表示同步状态

    • state 的含义由具体实现类来定义。例如,在ReentrantLock中,它表示锁的重入次数;在Semaphore中,它表示剩余的许可数量。
    • AQS通过getState(), setState(), compareAndSetState()这三个方法来原子地操作这个状态。
  2. 一个 FIFO 的线程等待队列 (CLH 队列):

    • 这是一个双向链表,用于存放所有等待获取同步状态的线程。
    • 当一个线程获取锁失败后,它就会被封装成一个Node节点,加入到这个队列的尾部并被挂起,等待被唤醒。

2.3 底层实现:线程如何排队与唤醒?

这是AQS的精髓所在,也是面试深度考察点。

  1. 线程安全入队

    • 当一个新线程获取锁失败需要入队时,它会被封装成一个Node节点。
    • AQS通过自旋(for循环)+ CAS(Compare-And-Swap)这种无锁的方式,来原子性地将新节点添加到等待队列的队尾。这避免了在入队这个高并发操作上加锁,提高了效率。
  2. 线程挂起(阻塞)

    • 线程成功入队后,AQS会调用 LockSupport.park(this) 方法将当前线程挂起。
    • LockSupport是JUC包中的一个工具类,它提供的park()/unpark()相比于Object.wait()/notify(),优点在于无需获取对象的监视器锁,并且可以unparkpark而不会丢失信号。
  3. 线程唤醒

    • 当持有锁的线程调用release()方法释放同步状态时,它会通过LockSupport.unpark()方法唤醒等待队列头部的下一个节点所对应的线程
    • 被唤醒的线程会再次尝试获取同步状态(state)。

2.4 独占模式 vs. 共享模式

AQS支持两种资源共享模式:

  • 独占模式 (Exclusive Mode):资源在同一时刻只能被一个线程持有。

    • 例子ReentrantLock
  • 共享模式 (Shared Mode):资源在同一时刻可以被多个线程持有。

    • 例子Semaphore(信号量)、CountDownLatchReadWriteLock中的读锁。

2.5 需要重写的核心方法 (模板方法)

开发者在实现自定义同步器时,需要根据选择的模式重写以下核心方法:

  • 独占模式

    • tryAcquire(int arg): 尝试以独占方式获取资源。
    • tryRelease(int arg): 尝试以独占方式释放资源。
    • isHeldExclusively(): 判断当前线程是否是独占持有资源。
  • 共享模式

    • tryAcquireShared(int arg): 尝试以共享方式获取资源。
    • tryReleaseShared(int arg): 尝试以共享方式释放资源。

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

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

立即咨询