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

相关推荐
言慢行善25 分钟前
sqlserver模糊查询问题
java·数据库·sqlserver
专吃海绵宝宝菠萝屋的派大星31 分钟前
使用Dify对接自己开发的mcp
java·服务器·前端
大数据新鸟1 小时前
操作系统之虚拟内存
java·服务器·网络
Tong Z1 小时前
常见的限流算法和实现原理
java·开发语言
凭君语未可1 小时前
Java 中的实现类是什么
java·开发语言
He少年1 小时前
【基础知识、Skill、Rules和MCP案例介绍】
java·前端·python
克里斯蒂亚诺更新1 小时前
myeclipse的pojie
java·ide·myeclipse
迷藏4941 小时前
**eBPF实战进阶:从零构建网络流量监控与过滤系统**在现代云原生架构中,**网络可观测性**和**安全隔离**已成为
java·网络·python·云原生·架构
迷藏4941 小时前
**发散创新:基于Solid协议的Web3.0去中心化身份认证系统实战解析**在Web3.
java·python·web3·去中心化·区块链
qq_433502181 小时前
Codex cli 飞书文档创建进阶实用命令 + Skill 创建&使用 小白完整教程
java·前端·飞书