首页 > 解决方案 > 访问非易失性变量的 Java 内存模型、易失性和同步块

问题描述

鉴于以下 Java 8 代码片段将供应商转换为缓存供应商,该供应商仅调用底层供应商一次并从今以后返回缓存值:

@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;
  };
}

这段代码根本不是为并发访问而设计的。该memoizing方法返回的记忆供应商可以由两个处理器上运行的两个独立线程并行访问。

为了使这个线程安全,可以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;
    }
  };
}

现在我想知道,由于SupplierMemoBox.supplier没有标记volatile,是否仍然会发生进入监视器的线程box读取陈旧变量的情况,box.supplier或者是否通过对象上的同步阻止发生这种情况box(即,这是否使对成员字段的所有访问安全?) . 或者是否有其他一些使它安全的技巧,即所有从进入监视器的线程发生的读取都保证不会过时?还是根本不安全?

标签: javamultithreadingjava-memory-model

解决方案


安全性由传递的发生前关系定义,如下所示:

17.4.5。订单前发生

两个动作可以通过happens-before关系排序。如果一个动作发生在另一个动作之前,那么第一个动作对第二个动作可见并在第二个动作之前排序。

如果我们有两个动作xy,我们写hb(x, y)来表示x 发生在 y 之前

  • 如果 x 和 y 是同一线程的操作,并且 x 在程序顺序中位于 y 之前,则为 hb(x, y)
  • 从对象的构造函数的末尾到该对象的终结器(第 12.6 节)的开头有一条发生前边缘。
  • 如果动作x 与后续动作y同步,那么我们也有hb(x, y)
  • 如果hb(x, y)hb(y, z),则hb(x, z)

上一节说

监视器m上的解锁操作与 m上的所有后续锁定操作同步(其中“后续”根据同步顺序定义)。

这可以得出规范还明确指出的内容:

从上面的定义可以得出:

  • 监视器上的解锁发生在该监视器上的每个后续锁定之前。

…</p>

我们可以将这些规则应用于您的程序:

  • null分配给 的第一个线程box.supplier在释放监视器(离开synchronized (box) { … })块之前执行此操作。由于第一个项目符号,这是在线程本身内排序的(“如果 x 和 y 是同一线程的操作,并且 x 在程序顺序中位于 y 之前,则hb(x, y) ”)
  • 随后获取相同监视器(进入synchronized (box) { … })块的第二个线程与第一个线程释放监视器具有发生前的关系(如上所述,“监视器上的解锁发生在该监视器上的每个后续锁定之前”)
  • box.supplier由于程序顺序,第二个线程对块内变量的读取synchronized再次与监视器的获取一起排序(“如果 x 和 y 是同一线程的操作,并且 x 在程序顺序中位于 y 之前,则hb(x, y) ”)
  • 由于最后一条规则“如果hb(x, y)hb(y, z),则hb(x, z) ”,现在可以组合上述三个关系。这种传递性使我们可以得出结论,在对变量的写入和随后的变量读取之间存在线程安全的顺序nullbox.supplier两者box.supplier都在同一个对象synchronized的块内。

box.supplier请注意,这与我们使用的对象的成员变量这一事实无关synchronized。重要的方面是两个线程都使用相同的对象synchronized来建立一个排序,该排序由于传递性规则而与其他操作交互。

但是在我们想要访问其成员的对象上进行同步是一个有用的约定,因为它可以更容易地确保所有线程都使用相同的对象进行同步。尽管如此,所有线程都必须遵守相同的约定才能使其工作。

作为反例,请考虑以下代码:

List<SomeType> list = …;

线程 1:

synchronized(list) {
    list.set(1, new SomeType(…));
}

线程 2:

List<SomeType> myList = list.subList(1, 2);

synchronized(list) {
    SomeType value = myList.get(0);
    // process value
}

在这里,线程 2 不myList用于同步是至关重要的,尽管我们使用它来访问内容,因为它是一个不同的对象。线程 2 仍然必须使用原始列表实例进行同步。这是一个真正的问题synchronizedList,其文档通过实例访问列表的示例进行了演示,该Iterator实例仍然必须通过在List实例上同步来保护。


推荐阅读