You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java同步块访问非volatile变量的线程安全性疑问

线程安全的缓存Supplier实现与并发安全性分析

原始非线程安全的缓存Supplier实现

这段Java 8代码可以将普通Supplier转换为缓存型实现,仅调用底层Supplier一次,后续返回缓存值,但仅适用于单线程场景:

@AllArgsConstructor
private final static class SupplierMemoBox<T> {
    private Supplier<T> supplier;
    private T value;
}

public static <T> Supplier<T> memoizing(@Nonnull final Supplier<T> supplier) {
    Objects.requireNonNull(supplier, "'supplier' must not be null");
    final SupplierMemoBox<T> box = new SupplierMemoBox<>(supplier, null);
    return () -> {
        if (box.supplier != null) {
            box.value = box.supplier.get();
            box.supplier = null;
        }
        return box.value;
    };
}

该实现未考虑并发访问场景,若多个线程并行调用返回的缓存Supplier,可能出现多次调用底层supplier.get()的情况,或者读取到未初始化的value。

加同步块的线程安全改进版本

为解决并发问题,我们可以对box对象加同步块,确保初始化逻辑的原子性:

public static <T> Supplier<T> memoizing(@Nonnull final Supplier<T> supplier) {
    Objects.requireNonNull(supplier, "'supplier' must not be null");
    final SupplierMemoBox<T> box = new SupplierMemoBox<>(supplier, null);
    return () -> {
        synchronized (box) {
            if (box.supplier != null) {
                box.value = box.supplier.get();
                box.supplier = null;
            }
            return box.value;
        }
    };
}

关于同步块与volatile的疑问解答

咱们先把Java中synchronized块的核心内存语义搞清楚,这是解答问题的关键:

  • 当线程进入synchronized块时,会强制清空该线程的本地缓存,所有后续对共享变量的读取都会直接从主内存获取最新值;
  • 当线程退出synchronized块时,会强制将该线程本地缓存中的所有修改刷新到主内存,确保其他线程能看到这些修改。

基于这个语义,我们逐个解答你的问题:

  1. 进入box对象监视器的线程是否仍可能读取到该变量的过期值?
    完全不会。进入同步块的动作会触发内存屏障,强制线程从主内存拉取supplier和value的最新值,不会使用本地缓存里的过期副本。

  2. 同步块是否能保证所有成员字段访问的安全性?
    是的。只要所有对box内部字段(supplier和value)的读写操作都被包裹在同一个锁(box对象)的同步块中,就能同时保证这些操作的原子性(初始化逻辑不会被拆分执行)、可见性(一个线程的修改对其他线程立即可见)和有序性(不会出现指令重排导致的异常逻辑)。

  3. 是否存在其他机制确保读取不会过期?
    核心就是synchronized自带的内存屏障机制:进入同步块的load屏障和退出同步块的store屏障,这两个屏障直接保证了线程本地缓存和主内存的一致性,不需要额外的机制。

  4. 该实现仍不安全?
    这个实现是线程安全的,但有个前提:底层的supplier.get()方法本身是线程安全的。如果get()内部存在非线程安全的操作,那即使加了同步块,也可能出现问题,但这属于Supplier本身的问题,和当前的缓存逻辑无关。除此之外,这个实现完美符合“仅初始化一次、后续返回缓存值”的需求,并发场景下表现稳定。

内容的提问来源于stack exchange,提问作者scravy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 09:26:23