对 Java 中的安全发布和可见性感到困惑,尤其是不可变对象
Confused about safe publishing and visibility in Java, especially with Immutable Objects
当我阅读 Brian Goetz 的 Java 并发实践时,我记得他在关于可见性的章节中说过 "Immutable objects, on the other hand, can be safely accessed even when synchronization is not used to publish the object reference"。
我认为这意味着如果您发布一个不可变对象,所有字段(包括可变最终引用)对可能使用它们的其他线程都是可见的,并且至少在该对象完成构造时是最新的。
现在,我在 https://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html 中读到
"Now, having said all of this, if, after a thread constructs an immutable object (that is, an object that only contains final fields), you want to ensure that it is seen correctly by all of the other thread, you still typically need to use synchronization. There is no other way to ensure, for example, that the reference to the immutable object will be seen by the second thread. The guarantees the program gets from final fields should be carefully tempered with a deep and careful understanding of how concurrency is managed in your code."
他们似乎相互矛盾,我不确定该相信哪个。
我还读到,如果所有字段都是最终字段,那么即使对象不是说不可变的,我们也可以确保安全发布。
例如,由于这个保证,我一直认为 Brian Goetz 的并发实践中的这段代码在发布这个 class 的对象时是可以的。
@ThreadSafe
public class MonitorVehicleTracker {
@GuardedBy("this")
private final Map<String, MutablePoint> locations;
public MonitorVehicleTracker(
Map<String, MutablePoint> locations) {
this.locations = deepCopy(locations);
}
public synchronized Map<String, MutablePoint> getLocations() {
return deepCopy(locations);
}
public synchronized MutablePoint getLocation(String id) {
MutablePoint loc = locations.get(id);
return loc == null ? null : new MutablePoint(loc);
}
public synchronized void setLocation(String id, int x, int y) {
MutablePoint loc = locations.get(id);
if (loc == null)
throw new IllegalArgumentException("No such ID: " + id);
loc.x = x;
loc.y = y;
}
private static Map<String, MutablePoint> deepCopy(
Map<String, MutablePoint> m) {
Map<String, MutablePoint> result =
new HashMap<String, MutablePoint>();
for (String id : m.keySet())
result.put(id, new MutablePoint(m.get(id)));
return Collections.unmodifiableMap(result);
}
}
public class MutablePoint { /* Listing 4.5 */ }
例如,在此代码示例中,如果最终保证为假并且线程创建了此 class 的实例,然后对该对象的引用不为空,但字段位置为空,该怎么办当时另一个线程使用 class?
再一次,我不知道哪个是正确的,或者我是否碰巧误解了这篇文章或 Goetz
这个问题之前已经回答过几次了,但我觉得其中很多回答都不充分。参见:
- Effectively Immutable Object
- 等...
简而言之,Goetz 在链接的 JSR 133 FAQ 页面中的声明更多 "correct",虽然不是您所想的那样。
当 Goetz 说不可变对象即使在没有同步的情况下发布时也可以安全使用时,他的意思是说对不同线程 可见 的不可变对象保证保留其原始内容state/invariants,其他一切保持不变。换句话说,不需要正确同步发布来保持状态一致性。
在 JSR-133 FAQ 中,当他说:
you want to ensure that it is seen correctly by all of the other thread (sic)
他指的不是不可变对象的状态。他的意思是你必须同步发布才能让另一个线程看到对不可变对象的引用。这两个声明所谈论的内容有细微差别:JCIP 指的是状态一致性,FAQ 页面指的是访问不可变对象的引用。
您提供的代码示例实际上与 Goetz 在这里所说的任何事情都没有关系,但为了回答您的问题,正确初始化的 final
字段将保持其预期值,如果对象是 正确初始化(注意初始化和发布之间的区别)。代码示例还同步对 locations
字段的访问,以确保对 final
字段的更新是线程安全的。
事实上,为了进一步详细说明,我建议您查看 JCIP 清单 3.13 (VolatileCachedFactorizer
)。请注意,尽管 OneValueCache
是不可变的,但它存储在 volatile
字段中。为了说明 FAQ 语句,如果没有 volatile
,VolatileCachedFactorizer
将无法正常工作 。 "Synchronization" 指的是使用 volatile
字段以确保对其所做的更新对其他线程可见。
说明第一个 JCIP 语句的一个好方法是删除 volatile
。在这种情况下,CachedFactorizer
将不起作用。考虑一下:如果一个线程设置了一个新的缓存值,但另一个线程试图读取该值并且该字段不是 volatile
怎么办? reader 可能 看不到更新后的 OneValueCache
。但是,回想一下 Goetz 指的是不可变对象的 state,如果 reader 线程碰巧看到 OneValueCache
的最新实例存储在cache
,那么该实例的状态将是可见的并正确构造。
所以虽然有可能丢失对cache
的更新,但是不可能丢失OneValueCache
的状态,如果它被读取的话,因为它是不可变的。我建议阅读随附的文字说明 "volatile reference used to ensure timely visibility."
作为最后一个例子,考虑 a singleton that uses FinalWrapper
for thread safety。请注意,FinalWrapper 实际上是不可变的(取决于单例是否可变),并且 helperWrapper
字段实际上是非易失性的。回想第二个常见问题解答声明,访问引用需要同步,这个"correct"实现怎么可能是正确的!?
事实上, 可以在此处执行此操作,因为线程没有必要立即查看 helperWrapper
的最新值。如果 helperWrapper
持有的值是非空的,那就太好了!我们的第一个 JCIP 语句保证 FinalWrapper
的状态是一致的,并且我们有一个完全初始化的 Foo
单例,可以很容易地返回。如果该值实际为null,则有2种可能:第一种可能是第一次调用,还没有初始化;其次,它可能只是一个过时的值。
在第一次调用的情况下,将在同步上下文中再次检查字段本身,如第二个 FAQ 语句所建议的那样。会发现这个值还是null,会初始化一个新的FinalWrapper
并同步发布
在它只是一个陈旧值的情况下,通过进入同步块,线程可以设置一个先行写入字段的先行顺序。根据定义,如果一个值是陈旧的,那么一些作者已经写入了 helperWrapper
字段,而当前线程还没有看到它。通过进入同步块,与之前的写入建立了先行关系,因为根据我们的第一个场景,真正未初始化的 helperWrapper
将由同一个锁初始化。因此,一旦方法进入同步上下文并获得最新的非空值,它就可以通过重新读取来恢复。
我希望我的解释和我给出的附带示例能够为您解决问题。
当我阅读 Brian Goetz 的 Java 并发实践时,我记得他在关于可见性的章节中说过 "Immutable objects, on the other hand, can be safely accessed even when synchronization is not used to publish the object reference"。
我认为这意味着如果您发布一个不可变对象,所有字段(包括可变最终引用)对可能使用它们的其他线程都是可见的,并且至少在该对象完成构造时是最新的。
现在,我在 https://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html 中读到 "Now, having said all of this, if, after a thread constructs an immutable object (that is, an object that only contains final fields), you want to ensure that it is seen correctly by all of the other thread, you still typically need to use synchronization. There is no other way to ensure, for example, that the reference to the immutable object will be seen by the second thread. The guarantees the program gets from final fields should be carefully tempered with a deep and careful understanding of how concurrency is managed in your code."
他们似乎相互矛盾,我不确定该相信哪个。
我还读到,如果所有字段都是最终字段,那么即使对象不是说不可变的,我们也可以确保安全发布。 例如,由于这个保证,我一直认为 Brian Goetz 的并发实践中的这段代码在发布这个 class 的对象时是可以的。
@ThreadSafe
public class MonitorVehicleTracker {
@GuardedBy("this")
private final Map<String, MutablePoint> locations;
public MonitorVehicleTracker(
Map<String, MutablePoint> locations) {
this.locations = deepCopy(locations);
}
public synchronized Map<String, MutablePoint> getLocations() {
return deepCopy(locations);
}
public synchronized MutablePoint getLocation(String id) {
MutablePoint loc = locations.get(id);
return loc == null ? null : new MutablePoint(loc);
}
public synchronized void setLocation(String id, int x, int y) {
MutablePoint loc = locations.get(id);
if (loc == null)
throw new IllegalArgumentException("No such ID: " + id);
loc.x = x;
loc.y = y;
}
private static Map<String, MutablePoint> deepCopy(
Map<String, MutablePoint> m) {
Map<String, MutablePoint> result =
new HashMap<String, MutablePoint>();
for (String id : m.keySet())
result.put(id, new MutablePoint(m.get(id)));
return Collections.unmodifiableMap(result);
}
}
public class MutablePoint { /* Listing 4.5 */ }
例如,在此代码示例中,如果最终保证为假并且线程创建了此 class 的实例,然后对该对象的引用不为空,但字段位置为空,该怎么办当时另一个线程使用 class?
再一次,我不知道哪个是正确的,或者我是否碰巧误解了这篇文章或 Goetz
这个问题之前已经回答过几次了,但我觉得其中很多回答都不充分。参见:
- Effectively Immutable Object
- 等...
简而言之,Goetz 在链接的 JSR 133 FAQ 页面中的声明更多 "correct",虽然不是您所想的那样。
当 Goetz 说不可变对象即使在没有同步的情况下发布时也可以安全使用时,他的意思是说对不同线程 可见 的不可变对象保证保留其原始内容state/invariants,其他一切保持不变。换句话说,不需要正确同步发布来保持状态一致性。
在 JSR-133 FAQ 中,当他说:
you want to ensure that it is seen correctly by all of the other thread (sic)
他指的不是不可变对象的状态。他的意思是你必须同步发布才能让另一个线程看到对不可变对象的引用。这两个声明所谈论的内容有细微差别:JCIP 指的是状态一致性,FAQ 页面指的是访问不可变对象的引用。
您提供的代码示例实际上与 Goetz 在这里所说的任何事情都没有关系,但为了回答您的问题,正确初始化的 final
字段将保持其预期值,如果对象是 正确初始化(注意初始化和发布之间的区别)。代码示例还同步对 locations
字段的访问,以确保对 final
字段的更新是线程安全的。
事实上,为了进一步详细说明,我建议您查看 JCIP 清单 3.13 (VolatileCachedFactorizer
)。请注意,尽管 OneValueCache
是不可变的,但它存储在 volatile
字段中。为了说明 FAQ 语句,如果没有 volatile
,VolatileCachedFactorizer
将无法正常工作 。 "Synchronization" 指的是使用 volatile
字段以确保对其所做的更新对其他线程可见。
说明第一个 JCIP 语句的一个好方法是删除 volatile
。在这种情况下,CachedFactorizer
将不起作用。考虑一下:如果一个线程设置了一个新的缓存值,但另一个线程试图读取该值并且该字段不是 volatile
怎么办? reader 可能 看不到更新后的 OneValueCache
。但是,回想一下 Goetz 指的是不可变对象的 state,如果 reader 线程碰巧看到 OneValueCache
的最新实例存储在cache
,那么该实例的状态将是可见的并正确构造。
所以虽然有可能丢失对cache
的更新,但是不可能丢失OneValueCache
的状态,如果它被读取的话,因为它是不可变的。我建议阅读随附的文字说明 "volatile reference used to ensure timely visibility."
作为最后一个例子,考虑 a singleton that uses FinalWrapper
for thread safety。请注意,FinalWrapper 实际上是不可变的(取决于单例是否可变),并且 helperWrapper
字段实际上是非易失性的。回想第二个常见问题解答声明,访问引用需要同步,这个"correct"实现怎么可能是正确的!?
事实上, 可以在此处执行此操作,因为线程没有必要立即查看 helperWrapper
的最新值。如果 helperWrapper
持有的值是非空的,那就太好了!我们的第一个 JCIP 语句保证 FinalWrapper
的状态是一致的,并且我们有一个完全初始化的 Foo
单例,可以很容易地返回。如果该值实际为null,则有2种可能:第一种可能是第一次调用,还没有初始化;其次,它可能只是一个过时的值。
在第一次调用的情况下,将在同步上下文中再次检查字段本身,如第二个 FAQ 语句所建议的那样。会发现这个值还是null,会初始化一个新的FinalWrapper
并同步发布
在它只是一个陈旧值的情况下,通过进入同步块,线程可以设置一个先行写入字段的先行顺序。根据定义,如果一个值是陈旧的,那么一些作者已经写入了 helperWrapper
字段,而当前线程还没有看到它。通过进入同步块,与之前的写入建立了先行关系,因为根据我们的第一个场景,真正未初始化的 helperWrapper
将由同一个锁初始化。因此,一旦方法进入同步上下文并获得最新的非空值,它就可以通过重新读取来恢复。
我希望我的解释和我给出的附带示例能够为您解决问题。