Java并发核心机制深度拆解——从ConcurrentHashMap到Happens-Before

第一部分:ConcurrentHashMap的演进与实现原理

ConcurrentHashMap在JDK 1.7和1.8中有什么重要改进?

ConcurrentHashMap在JDK 1.7到1.8版本有重大重构:

  • 数据结构:由1.7的Segment数组+HashEntry改为Node数组+链表/红黑树
  • 同步策略:由ReentrantLock锁住整个Segment改为仅对每个桶的头部节点添加synchronized,并配合大量CAS操作
  • 锁粒度:减小锁竞争的概率

除此之外,1.8版本还有其他优化:

  1. get()操作无需加锁:因为Node节点的val和next字段被volatile修饰
  2. 多线程扩容:提升了扩容效率
  3. size()方法优化size()方法采用分桶计数(CounterCell),通过累加各部分的计数来获取总量,避免了全局锁,实现了高并发下的精确计数
    • 引入红黑树:当单个桶内的元素过多时,链表查询会退化为O(n)。1.8版在特定条件下将链表转换为红黑树,将查询复杂度优化为O(log n),有效解决了哈希冲突严重时的性能问题

第二部分:volatile与原子操作

volatile 保证了可见性,却无法保证操作的原子性。

以经典的"读-改-写"场景------计数器递增 i++ 为例,它实际上包含三个独立步骤:

  1. 读取当前值
  2. 计算新值(当前值+1)
  3. 写入新值

如果 ivolatile 的,两个线程A和B几乎同时执行 i++,可能出现以下交错执行序列:

  1. 线程A读取 i=0
  2. 线程B读取 i=0因为A还未写入,B读到了旧值
  3. 线程A计算 0+1=1 并写入,i 变为 1(且立刻对所有线程可见)
  4. 线程B计算 0+1=1 并写入,i 再次变为 1

最终结果 i=1,而不是正确的 2volatile 保证了B在第4步能立刻看到A在第3步写入的 1,但无法阻止B在第2步已经读取了旧的 0。这就是原子性缺失导致的线程安全问题。

既然volatile不足以保证复合操作的原子性,那有哪些解决方案?

保证原子性的核心思想是将多个操作捆绑成一个不可分割的整体,方法包括:

  1. 加互斥锁:使用synchronized或ReentrantLock

    • 优点:简单安全
    • 缺点:性能消耗相对较大
  2. Atomic原子类:如AtomicInteger进行CAS操作

    • 优点:性能通常比加锁好
    • 缺点:高并发下CAS自旋可能消耗CPU,且只适用于单个变量
  3. 并发容器提供的原子方法:如ConcurrentHashMap的putIfAbsent

    • 优点:简单高效
    • 缺点:依赖容器,不够灵活

第三部分:CAS与ABA问题

什么是ABA问题?如何解决?

ABA问题是指在CAS操作时,一个值从A变成B又变回A,线程误以为状态没有改变。

示例:张三借李四的钱,李四账户余额为0。张三还款后改为100,又被王五取走,账户余额归0。张三看到余额为0就判断李四没有还钱。

解决方案

  • 添加版本号或时间戳:每次更改数值必须同步修改版本号

  • JDK工具

    • AtomicStampedReference:提供版本号
    • AtomicMarkableReference:提供boolean值判断状态是否被修改

第四部分:JMM与Happens-Before规则

volatile变量如何建立happens-before关系?有什么经典应用?

volatile变量遵循两个原则:

  1. 写操作时:所有其他变量的修改也会一起刷新
  2. 禁止重排序:写操作之前的操作不能重排序到写之后,读操作之后的操作不能重排序到读之前

经典应用:标志位开关

Java 复制代码
public class Example {
    // 核心:volatile 标志位开关
    private volatile boolean stopRequested = false;
    private int someState = 0; // 一个普通的非volatile变量

    // 工作线程
    private Thread workerThread = new Thread(() -> {
        while (!stopRequested) { // 【 volatile 读 】- 这里建立了hb关系
            // 模拟工作:读取或修改 someState
            someState++;
            try {
                Thread.sleep(100);
            } catch (InterruptedException ignored) {}
        }
        // 循环结束后,能看到主线程在设置stopRequested=true之前修改的所有状态
        System.out.println("Worker stopped. Final state: " + someState);
    });

    public void start() {
        workerThread.start();
    }

    // 主线程调用此方法请求停止
    public void stop() {
        // 1. 主线程可以在此更新一些最终状态(例如保存进度)
        someState = 100; // 普通写
        // 2. 【关键 volatile 写 】
        stopRequested = true; // 这个写操作与workerThread的读操作建立happens-before
        System.out.println("Stop requested sent.");
    }

    public static void main(String[] args) throws InterruptedException {
        Example example = new Example();
        example.start();
        Thread.sleep(1000); // 让工作线程运行1秒
        example.stop(); // 发送停止信号
        example.workerThread.join(); // 等待工作线程结束
    }
}

2.双重检查锁定单例模式

Java 复制代码
public class Singleton {
    // 关键:instance必须声明为volatile
    private static volatile Singleton instance;
    
    private final Config config;
    
    private Singleton() {
        // 这是一个昂贵的初始化过程
        this.config = loadConfigFromDB(); // 假设此操作很耗时
    }
    
    public static Singleton getInstance() {
        if (instance == null) {                     // 第一次检查(无锁,性能关键)
            synchronized (Singleton.class) {        // 加锁
                if (instance == null) {             // 第二次检查(防止重复创建)
                    // 非原子操作:1.分配内存 2.调用构造函数 3.将引用赋给instance
                    // 如果没有volatile,步骤2和3可能被重排序,导致其他线程看到未初始化完的对象(config为null)
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
    
    public Config getConfig() { return config; }
}

双重检查锁定单例中,为什么instance变量必须用volatile修饰?

synchronized保证同一时刻只有一个线程执行实例化代码块,但无法阻止JVM或CPU优化导致的指令重排序。

instance = new Singleton()在JVM层面分三步:

  1. 分配内存空间
  2. 初始化对象
  3. 将引用指向内存地址

没有volatile时可能发生的问题

  1. 线程A执行创建,JVM可能先执行步骤3(引用指向内存),但还未执行步骤2(初始化对象)
  2. 此时instance已不是null,但指向的是未初始化完成的半成品对象
  3. 线程B执行第一次检查,发现instance不是null,直接返回这个半成品对象
  4. 线程B使用这个对象时出错

volatile的关键作用

  • 禁止指令重排序:强制保证步骤2在步骤3之前执行
  • 保证可见性:线程A对instance的完整写入能立即被线程B看到

2. 一次性状态发布

当一个对象构造完成后,其状态不再改变,但需要被多个线程安全地访问。volatile提供了比synchronized更轻量级的发布方式。

java

arduino 复制代码
public class EventRouter {
    // 保存全局路由配置,启动后加载一次,之后只读。
    private volatile Map<String, String> routeConfig;
    
    public void init() {
        // 在单线程中完成所有复杂的、耗时的初始化
        Map<String, String> config = heavyWeightInitialization();
        // 全部完成后,一次性volatile写入,对所有线程立即可见
        this.routeConfig = config; // volatile写
    }
    
    public String getRoute(String event) {
        // 多线程并发读:volatile读,能立刻看到init线程写入的所有routeConfig内容
        Map<String, String> config = this.routeConfig; // volatile读
        return config.get(event);
    }
}

最佳实践 :配合不可变对象 (如Collections.unmodifiableMap包装的Map)使用,效果最佳。因为发布后状态不可变,读线程无需同步。

3. 低成本读写锁(读多写少)

在某些读远多于写、且写操作非常简单的场景,可以用volatile变量配合CAS(如AtomicInteger)模拟一个更轻量的读写锁。

java

csharp 复制代码
public class LightweightCoordinator {
    private volatile int epoch = 0; // "纪元"版本号
    private final AtomicInteger writeLock = new AtomicInteger(0);
    
    public void beginRead() {
        int snapshotEpoch;
        do {
            snapshotEpoch = epoch; // volatile读,获取当前"稳定"版本
            // 检查是否有写操作正在进行(通过CAS状态判断,此处简化)
        } while (isWritingInProgress()); // 如果正在写,则重试
        // 后续读操作基于snapshotEpoch进行
    }
    
    public void endRead() {
        // 通常无需操作
    }
    
    public void beginWrite() {
        // 通过CAS获取写锁
        while (!writeLock.compareAndSet(0, 1)) {
            // 自旋或后退
        }
        epoch++; // volatile写,递增纪元,强制所有读线程在下一次读取时看到新数据
        // ... 执行写操作
    }
    
    public void endWrite() {
        writeLock.set(0); // 释放写锁
    }
}

注意 :这是一种高级模式,仅适用于写操作极快、读操作可以容忍短暂重试的场景。多数情况下,应直接使用ReentrantReadWriteLockStampedLock

相关推荐
梦未13 小时前
Spring控制反转与依赖注入
java·后端·spring
喜欢流萤吖~13 小时前
Lambda 表达式
java
ZouZou老师13 小时前
C++设计模式之适配器模式:以家具生产为例
java·设计模式·适配器模式
曼巴UE514 小时前
UE5 C++ 动态多播
java·开发语言
VX:Fegn089514 小时前
计算机毕业设计|基于springboot + vue音乐管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·课程设计
程序员鱼皮14 小时前
刚刚,IDEA 免费版发布!终于不用破解了
java·程序员·jetbrains
Hui Baby14 小时前
Nacos容灾俩种方案对比
java
曲莫终15 小时前
Java单元测试框架Junit5用法一览
java
成富15 小时前
Chat Agent UI,类似 ChatGPT 的聊天界面,Spring AI 应用的测试工具
java·人工智能·spring·ui·chatgpt