Java并发编程:内存可见性与synchronized同步机制

前言

在Java并发编程中,理解内存模型和可见性问题是掌握多线程开发的基础。本文将重点围绕Java内存模型(JMM)以及synchronized关键字的内存语义展开,帮助读者建立扎实的并发编程基础。


一、并发与并行的区别

1.1 概念辨析

概念 定义 特点
并发 在一个时间段内多个任务交替执行 宏观同时,微观串行
并行 在同一个时刻多个任务同时执行 需要多核CPU支持

💡 重点理解:并发任务强调在一个时间段内同时执行,但单位时间内不一定同时在执行。在单CPU时代,所有任务都是并发执行的。

1.2 实际场景

并发:

现代多线程编程中,线程数量往往多于CPU核心数,因此我们通常称之为多线程并发编程而非并行编程。当系统配置双CPU时,线程A和线程B可以各自在不同CPU上真正并行运行。

并行:


二、Java内存模型(JMM)

2.1 抽象结构

Java内存模型规定:

  • 主内存:所有的变量都存放在主内存中

  • 工作内存:每个线程拥有自己的工作内存,线程读写变量时操作的是工作内存中的副本

复制代码

2.2 硬件层面的映射

在实际硬件架构中(以双核CPU为例):

对应关系

  • Java工作内存 → L1/L2 Cache 或 CPU寄存器

  • Java主内存 → 物理主内存


三、内存可见性问题

3.1 问题复现

假设线程A和线程B在不同CPU上执行,共同操作共享变量X,初始值为0,两级Cache初始均为空:

步骤 操作 CPU-A Cache CPU-B Cache 主内存
初始 - X=0
1 A读取X(两级Cache未命中,从主内存加载) X=0 X=0
2 A修改X=1,写入两级Cache并刷新到主内存 X=1 X=1
3 B读取X(L1未命中,L2命中,返回1) X=1 X=1 X=1
4 B修改X=2,写入L1、L2,更新主内存 X=1 ❌ X=2 X=2
5 A再次读取X(L1命中,返回旧值1) X=1 ❌ X=2 X=2

⚠️ 问题出现:步骤5中,线程A获取到的X仍然是1,无法感知线程B已经将其修改为2。

3.2 可见性问题的本质

内存可见性问题:一个线程对共享变量的修改,未能及时被其他线程看到。

产生原因:

  1. CPU缓存的存在:每个CPU核心拥有独立的缓存

  2. 编译器优化:指令重排序可能导致执行顺序变化

  3. 处理器优化:写缓冲区和无效化队列的延迟


四、synchronized的内存语义

4.1 核心语义

synchronized关键字在Java内存模型中具有明确的内存语义:

操作 内存行为
进入synchronized块 清除工作内存中该块内使用到的变量,直接从主内存读取
退出synchronized块 将synchronized块内对共享变量的修改刷新到主内存

4.2 语义图解

text

复制代码
进入synchronized块:
┌─────────────────────────────────────────────────────┐
│  线程工作内存                   主内存               │
│  ┌──────────┐                  ┌──────────┐        │
│  │ 变量副本 │  ──(清除)──►      │ 共享变量 │        │
│  │ (可能过期)│                  │ (最新值) │        │
│  └──────────┘                  └────┬─────┘        │
│                                      │              │
│                           (从主内存重新读取)          │
│                                      ▼              │
│                               ┌──────────┐          │
│                               │ 最新副本 │          │
│                               └──────────┘          │
└─────────────────────────────────────────────────────┘

退出synchronized块:
┌─────────────────────────────────────────────────────┐
│  线程工作内存                   主内存               │
│  ┌──────────┐                  ┌──────────┐        │
│  │ 修改后的 │  ──(刷新)──►      │ 共享变量 │        │
│  │ 变量值   │                  │ (更新后) │        │
│  └──────────┘                  └──────────┘        │
└─────────────────────────────────────────────────────┘

4.3 解决可见性问题的原理

回到3.1的问题场景,使用synchronized后:

java 复制代码
public class SynchronizedSolution {
    private int x = 0;
    
    public synchronized void updateX(int newValue) {
        // 进入同步块:清除x的工作内存副本
        // 直接从主内存读取最新值
        this.x = newValue;
        // 退出同步块:将x的新值刷新到主内存
    }
    
    public synchronized int getX() {
        // 进入同步块:清除工作内存,从主内存读取
        return this.x;
        // 退出同步块:刷新到主内存
    }
}

执行流程

  1. 线程A调用updateX(1) → 从主内存读取最新x值 → 修改为1 → 退出时刷新到主内存

  2. 线程B调用updateX(2) → 进入时清除缓存 → 从主内存读取到1 → 修改为2 → 退出时刷新到主内存

  3. 线程A再次调用getX() → 进入时清除缓存 → 从主内存读取到2 → ✅ 可见性问题解决

4.4 双重作用

synchronized关键字提供两个重要保证:

作用 说明
解决可见性 通过进入时清除缓存、退出时刷新缓存的内存语义
保证原子性 同一时刻只有一个线程能执行同步块/方法

4.5 使用示例

java 复制代码
public class Counter {
    private int count = 0;
    
    // 同步方法 - 保证原子性和可见性
    public synchronized void increment() {
        count++;  // 读取-修改-写入,三步操作整体原子执行
    }
    
    public synchronized int getCount() {
        return count;  // 保证读取到最新值
    }
}

public class SharedResource {
    private boolean initialized = false;
    private Object data = null;
    
    // 同步块 - 更细粒度的控制
    public void init(Object newData) {
        synchronized (this) {
            if (!initialized) {
                data = newData;
                initialized = true;
                // 修改自动刷新到主内存
            }
        }
    }
    
    public Object getData() {
        synchronized (this) {
            // 自动从主内存读取最新值
            return initialized ? data : null;
        }
    }
}

五、synchronized的性能考虑

5.1 性能开销

📌 重要提醒synchronized关键字会引起线程上下文切换并带来线程调度开销。

主要开销来源:

  1. 获取/释放锁的系统调用

  2. 线程阻塞与唤醒:未获取到锁的线程会进入阻塞状态

  3. 上下文切换:保存和恢复线程状态

  4. 内存屏障:保证可见性引入的缓存刷新操作

5.2 优化建议

java 复制代码
// ❌ 不推荐:同步范围过大
public synchronized void badExample() {
    // 大量非共享变量的计算操作
    longRunningTask();
    sharedValue++;
}

// ✅ 推荐:仅同步必要部分
public void goodExample() {
    // 非共享操作在同步块外执行
    longRunningTask();
    synchronized (this) {
        sharedValue++;
    }
}

六、常见问题与最佳实践

6.1 常见误区

误区 正确理解
synchronized只保证互斥 ✅ 同时保证互斥和可见性
进入同步块后所有变量都从主内存读取 ✅ 仅同步块内使用到的变量
退出同步块后所有变量都刷新到主内存 ✅ 仅同步块内修改过的变量

6.2 最佳实践

java 复制代码
public class BestPractice {
    // 1. 同步范围最小化
    private int counter = 0;
    public void increment() {
        // 只同步必要的操作
        synchronized (this) {
            counter++;
        }
    }
    
    // 2. 读操作也需要同步(保证可见性)
    public synchronized int getCounter() {
        return counter;
    }
    
    // 3. 使用私有锁对象(避免外部干扰)
    private final Object lock = new Object();
    public void safeMethod() {
        synchronized (lock) {
            // 安全操作
        }
    }
    
    // 4. 避免在同步块内调用外部方法
    public void dangerMethod(OtherService service) {
        synchronized (this) {
            // ❌ 可能造成死锁或性能问题
            service.callback(this);
        }
    }
}

6.3 适用场景总结

场景 推荐方案
需要保证复合操作的原子性 synchronized
多个操作需要整体执行 synchronized
读写共享变量需要互斥 synchronized
简单状态标记(单次读写) 后续会介绍的volatile

七、总结

核心要点回顾

  1. Java内存模型:主内存 + 线程工作内存的抽象结构

  2. 可见性问题:缓存的存在导致一个线程的修改对另一个线程不可见

  3. synchronized内存语义

    • 进入:清除工作内存中的变量副本,从主内存读取

    • 退出:将对共享变量的修改刷新到主内存

  4. synchronized的双重作用:既保证原子性,又保证可见性

关键结论

🎯 synchronized通过进入时清除缓存、退出时刷新缓存的内存语义,完美解决了共享变量的内存可见性问题。同时,它也是Java中最基础的原子性保证机制

相关推荐
用户3959924940067 小时前
Java开发者接入大模型API实战:从0到聊天机器人
java
爱喝水的鱼丶8 小时前
SAP-ABAP:数据类型与数据对象(8篇) 第四篇:关系映射篇——从类型定义到对象实例的转化逻辑
开发语言·数据库·学习·sap·abap
JAVA面经实录9178 小时前
Java 多线程完整版学习文档(无遗漏终版)
java·面试
考虑考虑8 小时前
JDK26中的LazyConstant
java·后端·java ee
水无痕simon8 小时前
1. Guava 介绍
开发语言·python·guava
Devin~Y8 小时前
互联网大厂 Java 面试实录:JVM、Spring Boot、MyBatis、Redis、Kafka、Spring AI、K8s 全链路追问小Y
java·jvm·spring boot·redis·kafka·mybatis·spring security
摇滚侠8 小时前
SpringCloud 面试题 真正的 offer 偏方 Java 基础 Java 高级
java·spring·spring cloud
AI科技星8 小时前
全域数学公理:基于32维超复数与易经卦爻的宇宙大一统理论(整理版)
c语言·开发语言·线性代数·量子计算·agi
之歆8 小时前
DAY_13JavaScript DOM 操作完全指南:实战案例、性能优化与业务价值(下)
开发语言·前端·javascript·性能优化·ecmascript