给出以下Java 8 Code代码片段,将供应商转变为缓存供应商,该供应商仅调用基础供应商一次,并从此返回缓存值:
@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
方法返回的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
对象上的同步防止发生这种情况(即,是否会使对成员字段的所有访问都安全?)。或者是否有一些其他技巧使它安全,即从进入监视器的线程发生的所有读取都保证不会过时?或者它根本不安全?
答案 0 :(得分:2)
安全性由传递性 happens-before 关系定义如下:
17.4.5. Happens-before Order
可以通过先发生后关系来命令两个动作。如果一个动作发生在另一个动作之前,那么第一个动作对于第二个动作是可见的,并且在第二个动作之前排有序。
如果我们有两个动作 x 和 y ,我们写 hb(x,y)表示 x发生了-在y 之前。
- 如果x和y是同一线程的动作,并且x按程序顺序位于y之前,则 hb(x,y)。
- 从对象的构造函数的末端到该对象的终结器(第12.6节)的起点之间有一个 happens-before 边缘。
- 如果动作 x 与以下动作 y 同步,那么我们也有 hb(x,y)< / em>。
- 如果 hb(x,y)和 hb(y,z),则 hb(x,z)。
>
前面所述的
在监视器 m 上的解锁操作与 m 上的所有后续锁定操作同步(其中“后续”是根据同步定义的)订单)。
它可以得出规范也明确指出的内容:
根据以上定义,它是:
- 在监视器上的解锁在此监视器上的每个后续锁定发生之前。
...
我们可以将这些规则应用于您的程序:
null
分配给box.supplier
的第一个线程在释放监视器(离开synchronized (box) { … }
)块之前执行此操作。这是由于第一个项目符号而在线程本身中排序的(“如果x和y是同一线程的动作,并且x按程序顺序位于y之前,则 hb(x,y)”)< / li>
synchronized (box) { … }
块)的第二个线程与第一个线程释放监视器具有先发生关系(如上结论,“在监视器上解锁发生之前该监视器上的所有后续锁定”)box.supplier
块中的synchronized
变量的读取中又重新获得了监视器的顺序(“如果x和y是同一线程的动作,并且x按程序顺序排在y之前,然后是 hb(x,y)”)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
实例上进行同步来保护。
答案 1 :(得分:1)
是的,如果仅在i
内修改local_relevant_points
对象的属性,则它是线程安全的。但要注意不要重新分配整个对象值,即box
不是线程安全的(很多人因为未知原因而犯了这个错误)。
如果您想对synchronized (box) { }
进行非阻塞修改(即没有box = someValue
或类似的锁定),则将box.supplier
标记为volatile
会有所帮助。