Java同步块访问非volatile变量的线程安全性疑问
原始非线程安全的缓存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块时,会强制将该线程本地缓存中的所有修改刷新到主内存,确保其他线程能看到这些修改。
基于这个语义,我们逐个解答你的问题:
进入box对象监视器的线程是否仍可能读取到该变量的过期值?
完全不会。进入同步块的动作会触发内存屏障,强制线程从主内存拉取supplier和value的最新值,不会使用本地缓存里的过期副本。同步块是否能保证所有成员字段访问的安全性?
是的。只要所有对box内部字段(supplier和value)的读写操作都被包裹在同一个锁(box对象)的同步块中,就能同时保证这些操作的原子性(初始化逻辑不会被拆分执行)、可见性(一个线程的修改对其他线程立即可见)和有序性(不会出现指令重排导致的异常逻辑)。是否存在其他机制确保读取不会过期?
核心就是synchronized自带的内存屏障机制:进入同步块的load屏障和退出同步块的store屏障,这两个屏障直接保证了线程本地缓存和主内存的一致性,不需要额外的机制。该实现仍不安全?
这个实现是线程安全的,但有个前提:底层的supplier.get()方法本身是线程安全的。如果get()内部存在非线程安全的操作,那即使加了同步块,也可能出现问题,但这属于Supplier本身的问题,和当前的缓存逻辑无关。除此之外,这个实现完美符合“仅初始化一次、后续返回缓存值”的需求,并发场景下表现稳定。
内容的提问来源于stack exchange,提问作者scravy

