Java 内存模型、易失性和同步块访问非易失性变量

Java Memory Model, volatile, and synchronized blocks accessing non-volatile variables

给定以下 Java 8 代码片段,该代码将 Supplier 转变为缓存 Supplier,它只调用底层 Supplier 一次,并且 returns 此后的缓存值:

@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 对象上的同步阻止了这种情况的发生(即,这是否使对成员字段的所有访问都安全?)。或者是否有其他一些使其安全的技巧,即所有从进入监视器的线程发生的读取都保证不会过时?还是根本不安全?

是的,如果您仅在 synchronized (box) { } 内部修改 box 对象的属性,它是线程安全的。但注意不要重新分配整个对象值,即 box = someValue 不是线程安全的(很多人出于未知原因犯了这个错误)。

如果您想对其进行非阻塞修改(即没有 synchronized 或类似锁定),将 box.supplier 标记为 volatile 会有所帮助。

安全性由传递性 happens-before 关系定义如下:

17.4.5. Happens-before Order

Two actions can be ordered by a happens-before relationship. If one action happens-before another, then the first is visible to and ordered before the second.

If we have two actions x and y, we write hb(x, y) to indicate that x happens-before y.

  • If x and y are actions of the same thread and x comes before y in program order, then hb(x, y).
  • There is a happens-before edge from the end of a constructor of an object to the start of a finalizer (§12.6) for that object.
  • If an action x synchronizes-with a following action y, then we also have hb(x, y).
  • If hb(x, y) and hb(y, z), then hb(x, z).

前一节说

An unlock action on monitor m synchronizes-with all subsequent lock actions on m (where "subsequent" is defined according to the synchronization order).

这允许总结规范还明确说明的内容:

It follows from the above definitions that:

  • An unlock on a monitor happens-before every subsequent lock on that monitor.

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

  • 第一个分配 nullbox.supplier 的线程在释放监视器(离开 synchronized (box) { … })块之前执行此操作。由于第一个项目符号,这是在线程本身内排序的(“如果 x 和 y 是同一线程的操作,并且 x 在程序顺序中出现在 y 之前,则 hb(x, y) ”)
  • 第二个线程随后获取同一个监视器(进入 synchronized (box) { … })块)与第一个线程释放监视器(如上面总结道,“监视器上的解锁 发生在 该监视器上的每个后续锁定之前”)
  • 由于程序顺序,第二个线程读取synchronized块中的box.supplier变量再次与监视器的获取一起排序(“如果x和y是相同的动作线程和 x 在程序顺序中出现在 y 之前,然后 hb(x, y)”)
  • 由于最后一条规则,“如果hb(x, y) and hb(y, z) ,然后 hb(x, z)”。这种传递性使我们能够得出结论,在将 null 写入 box.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 实例上同步来保护。