线程可见性问题

线程可见性问题

Java 内存模型和这个MESI协议的关系 - deyang - 博客园

CPU缓存行(Cache Line)和MESI协议 - deyang - 博客园

问题提出

一个线程的修改,另一个线程可能永远看不到

假如A线程修改全局变量param,B线程不会立马看得到的。

int param = 0;

Thread updater = new Thread(() -> {
    try {
    	Thread.sleep(100); // 模拟准备时间
    } catch (InterruptedException e) {
    	e.printStackTrace();
	}
	param = 10;
});

Thread user = new Thread(() -> {
	log.info("打印param:{}",param);
});

user.start();
updater.start();
updater.join();
user.join();

// 问题:
// param 是普通局部变量,未用 volatile 修饰,不保证内存可见性。
// updater 线程修改 param = 10 后,user 线程可能仍读到旧值(如 0)。
// 虽然 join() 保证 updater 执行完,但无同步手段,JIT 编译优化或 CPU 缓存可能导致 user 未及时看到更新。
// 结论:
// 存在可见性问题,不能保证 user 一定读到 10。
// 解决方案:
// 将 param 改为 volatile int param 或使用原子类。

顺序基本正确:寄存器 → Store/Load Buffer → L1 → L2 → L3 → 内存

分点总结如下:

  1. ✅ 线程独有:
    • 寄存器(如程序计数器、栈指针等),线程切换时由操作系统保存/恢复上下文。
  2. ✅ 核心独有(每个核心私有):
    • Store/Load Buffer(每个核心独立)
    • L1 缓存(通常分为指令和数据缓存,每核私有)
    • L2 缓存(一般也是每核私有,部分架构可能共享)
  3. ✅ 核心共享:
    • L3 缓存(多核共享,通过一致性协议如 MESI 管理)
    • 主内存(所有核心共用)

📌 注意:具体架构可能有差异(如某些 ARM 或 Intel 多核共享 L2),但通用模型中 L1/L2 多为私有,L3 及内存为共享。

📌 注意:Store/Load Buffer 不是“存储层级”中的一级缓存,而是为提升性能而设的硬件缓冲结构,在体系结构图中常被忽略,但在并发编程(如内存屏障、可见性)中至关重要。

到哪一步了才能被其他线程看到?

修改数据的"可见性边界":
┌─────────────────────────────────────────────────────┐
│                   线程A修改数据                        │
│                  例如: param = 10;              │
└──────────────────────────┬──────────────────────────┘
                           │
                 在A线程的层级:                         可见性
                 ┌─────────────────┐                ┌────────────┐
                 │   寄存器         │ ← 仅A线程可见    │ 完全不可见   │
                 └─────────┬───────┘                └────────────┘
                           │
                 ┌─────────▼─────────┐              ┌────────────┐
                 │   Store Buffer    │ ← 仅A核心可见 │ 完全不可见   │
                 │   (写缓冲区)       │              └────────────┘
                 └─────────┬─────────┘
                           │ ←─── 关键边界!─────────────┐
                 ┌─────────▼─────────┐              │   │
                 │     L1缓存        │ ← A核心可见    │ 其他核心    │
                 │                   │              │ 可能看到     │
                 └─────────┬─────────┘              │ 也可能看不到 │
                           │                        │   │
                 ┌─────────▼─────────┐              │   │
                 │     L2缓存        │ ← A核心可见    │ 其他核心    │
                 │                   │              │ 可能看到     │
                 └─────────┬─────────┘              │   │
                           │                        │   │
                 ┌─────────▼─────────┐              │   │
                 │     L3缓存        │ ← 所有核心    │ 其他线程    │
                 │    (共享缓存)      │   都可能看到    │ 可能看到     │
                 └─────────┬─────────┘              │   │
                           │ ←─── 强可见边界!───────┘   │
                 ┌─────────▼─────────┐                  │
                 │     主内存         │ ← 完全可见        │ 所有线程    │
                 │     (RAM)         │                  │ 都能看到     │
                 └───────────────────┘                  └────────────┘

Store/Load Buffer 是导致可见性问题的关键因素之一,但不是唯一原因。分点说明:

  1. Store Buffer 导致写延迟
    • 核心写数据时先放入 Store Buffer,不会立即写入 L1 缓存。
    • 其他核心无法直接看到该更新,造成写不即时可见。
  2. Load Buffer 导致读旧值
    • 读操作可能绕过缓存从 Load Buffer 获取数据,若未及时同步,会读到过期值。
  3. 重排序(Reordering)加剧问题
    • 硬件和编译器可能对内存操作重排序,结合缓冲机制,使程序行为不符合预期。
  4. 缓存一致性协议(如 MESI)有延迟
    • 即使最终缓存一致,中间状态仍可能导致脏读或漏读。

📌 因此: Store/Load Buffer 是硬件层面引发可见性问题的核心机制,但需配合内存屏障(如 mfence)、volatile、原子操作等手段来强制刷新缓冲区、保证顺序性和可见性。

导致可见性问题的主要原因如下:

  1. Store Buffer 延迟写入
    • 核心写数据先存入 Store Buffer,不会立即写回 L1 缓存,其他核心无法看到最新值。
  2. Load Buffer 读取滞后
    • 读操作可能未从缓存刷新,导致读到旧数据。
  3. CPU 缓存层次结构
    • 每个核心有私有 L1/L2 缓存,更新未及时同步到其他核心。
  4. 重排序(Reordering)
    • 编译器或 CPU 为优化性能调整指令顺序,破坏程序顺序语义。
  5. 缓存一致性延迟
    • 即使使用 MESI 等协议,缓存同步存在时序窗口,仍可能读到过期值。

✅ 根本原因:多核各自拥有私有缓存与缓冲区(如 Store/Load Buffer),缺乏自动强同步机制。

📌 解决方案:使用 volatile、内存屏障、原子变量等强制可见性和顺序性。

1个线程只修改1个字段的问题

代码和问题提出

Map<Integer, Role> roleMap = new ConcurrentHashMap<>();
Thread A = new Thread(() -> {
    Role role = roleMap.get(roleId);
    // 这个线程永远不会修改name字段
    try {
        Thread.sleep(100); // 模拟操作时间
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    // 保存
    DataCenter.updateData(role);
});

Thread B = new Thread(() -> {
    Role role = roleMap.get(roleId);
    // 这个线程只修改name字段
    try {
        role.setRoleName("wdy");
        Thread.sleep(100); // 模拟操作时间
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    // 保存
    DataCenter.updateData(role);
});

A.start();
B.start();
A.join();
B.join();

会有可见性问题数据一致性问题

主要问题分析:

1.内存可见性问题

虽然 ConcurrentHashMap 保证了其内部操作的线程安全,但对于 Role 对象本身的字段修改,没有内存可见性保证

当线程B修改 role.setRoleName("wdy") 时:

  • 修改可能只写入到线程B的本地缓存(CPU缓存)
  • 线程A可能读取到旧的 roleName
  • 即使线程A在修改后读取,由于没有同步机制,也不能保证看到最新值

2.竞态条件(Race Condition)

两个线程并发读取同一个 Role 对象,然后各自操作并保存,会导致:

  • 线程A的修改(如果有)可能被线程B覆盖
  • 线程B的修改可能被线程A覆盖
  • 最终保存到 DataCenter 的数据可能是部分修改的结果

具体的内存可见性问题流程:

// 线程B(在CPU Core 1上执行)
role.setRoleName("wdy");
// 实际执行:this.roleName = "wdy";
没有同步时的执行流程:
  1. 线程B修改 roleName 字段
  2. 写入进入 Store Buffer(CPU Core 1的写缓冲区)
  3. Store Buffer可能不会立即刷新到L1 Cache
  4. 即使刷新到L1 Cache,其他CPU核心的 缓存一致性协议(MESI) 需要时间来同步
  5. 线程A(可能在CPU Core 2上)读取时,可能:
    • 从自己的L1/L2 Cache读取旧值
    • 或者通过缓存一致性协议获取新值,但时机不确定
Store/Load Buffer的作用
  • Store Buffer:暂存写入操作,避免CPU等待内存写入完成
  • Load Buffer:暂存读取请求,优化缓存未命中时的等待
  • 问题:这些缓冲区导致"写操作对其他CPU核心不可见"或"可见时机不确定"
有同步和无同步的区别
// 无同步 - 可能有问题
role.setRoleName("wdy");

// 有同步 - 保证可见性
synchronized(lock) {
    role.setRoleName("wdy");  // 隐式内存屏障
}
// 或者
volatile String roleName;  // volatile写自带内存屏障

竞态条件中"覆盖"的详细情况

场景还原

假设 Role 对象有两个字段:

class Role {
    String roleName;
    int level;
}

初始状态:

role.roleName = "old";
role.level = 1;
并发执行的时间线:
时间线    线程A                        线程B
--------------------------------------------------
t0     读取 role (roleName="old")    读取 role (roleName="old")
t1     读取 level=1                   修改 roleName="wdy"
t2     sleep 100ms                   sleep 100ms
t3     保存到 DataCenter              保存到 DataCenter
几种可能的覆盖情况:

情况1:线程B覆盖线程A

最终DataCenter数据:roleName="wdy", level=1
说明:线程A保存后,线程B又保存,覆盖了线程A的保存

情况2:线程A覆盖线程B

最终DataCenter数据:roleName="old", level=1
说明:线程B保存后,线程A又保存,覆盖了线程B的roleName修改

情况3:更复杂的覆盖(基于数据库操作)

如果 DataCenter.updateData() 是这样的:

void updateData(Role role) {
    // 假设是更新数据库
    db.update("UPDATE role SET name=?, level=? WHERE id=?", 
              role.getRoleName(), role.getLevel(), roleId);
}

可能的时间交错:

线程A: UPDATE role SET name='old', level=1 WHERE id=100
         ↑
线程B: UPDATE role SET name='wdy', level=1 WHERE id=100
         ↓
数据库最终:name='wdy', level=1  // B的UPDATE后执行

最危险的覆盖:部分字段覆盖

如果 DataCenter.updateData() 是这样的:

void updateData(Role role) {
    // 只更新有变化的字段(常见ORM框架行为)
    if (role.isNameChanged()) {
        db.update("UPDATE role SET name=? WHERE id=?", 
                  role.getRoleName(), roleId);
    }
    if (role.isLevelChanged()) {
        db.update("UPDATE role SET level=? WHERE id=?", 
                  role.getLevel(), roleId);
    }
}

可能产生数据损坏:

线程A: UPDATE role SET level=2 WHERE id=100  // 假设level变了
         ↓
线程B: UPDATE role SET name='wdy' WHERE id=100
         ↓
数据库结果:name='wdy', level=2  ✅ 看似正常...

但如果是:
线程A: 判断level未变,不更新level
线程B: 更新name='wdy'
结果:name='wdy', level=1  ✅

但如果线程A在读取后、保存前,level被其他线程修改:
初始:name='old', level=1
其他线程C: 修改level=99
线程A: 保存(认为level没变,实际数据库已变)
线程B: 保存name='wdy'
最终:name='wdy', level=99  ❌ 丢失了线程A对level的修改
解决方案的核心思想
// 错误的:共享可变对象 + 无同步
Role role = roleMap.get(roleId);  // 多个线程获取同一个可变对象

// 正确的模式1:同步访问
synchronized(lock) {
    Role role = roleMap.get(roleId);
    // 修改
    roleMap.put(roleId, role);  // 或直接修改
}

// 正确的模式2:不可变对象 + 原子更新
AtomicReference<Role> ref = roleMap.get(roleId);
Role newRole = createNewRoleBasedOn(ref.get());  // 创建新对象
ref.compareAndSet(oldRole, newRole);  // 原子替换

关键点:要么同步访问,要么使用不可变对象+原子引用,避免多个线程同时修改同一个可变对象的状态。

posted @ 2026-02-14 14:36  deyang  阅读(1)  评论(0)    收藏  举报