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.
…
我们可以将这些规则应用于您的程序:
- 第一个分配
null
到 box.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
实例上同步来保护。
给定以下 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.
…
我们可以将这些规则应用于您的程序:
- 第一个分配
null
到box.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
实例上同步来保护。