当我阅读Brian Goetz的Java Concurrency in Practice时,我记得他说“另一方面,即使在不使用同步来发布对象引用时,也可以安全地访问不可变对象”。
我认为这意味着如果你发布一个不可变对象,所有字段(包括可变的最终引用)对于可能使用它们的其他线程是可见的,并且至少是该对象何时完成构建的最新版本。
现在,我在https://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html读到了 “现在,说过所有这些,如果在一个线程构造一个不可变对象(即一个只包含最终字段的对象)之后,你想要确保所有其他线程都能正确看到它,你通常仍然是需要使用同步。没有其他方法可以确保,例如,第二个线程将看到对不可变对象的引用。程序从最终字段获得的保证应该仔细调整,并仔细了解如何在代码中管理并发。“
他们似乎互相矛盾,我不确定相信哪一个。
我还读过,如果所有字段都是最终字段,那么即使对象不是永久性的,我们也可以确保安全发布。 例如,我一直认为,由于这种保证,在发布此类对象时,Brian Goetz在实践中的并发性中的这段代码很好。
@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 */ }
例如,在此代码示例中,如果最终保证为false并且线程构成此类的实例然后对该对象的引用不为null,但在另一个线程使用时字段位置为null,该怎么办?那班?
再一次,我不知道哪个是正确的,或者我是否恰好误解了文章或Goetz
答案 0 :(得分:3)
这个问题之前已经回答了几次,但我觉得很多答案都不合适。参见:
简而言之,Goetz在链接的JSR 133常见问题解答页面中的陈述更多&#34;正确&#34;,虽然不是您想的方式。
当Goetz说即使在没有同步的情况下发布时,不可变对象也可以安全使用,他的意思是说,对于不同线程可见的不可变对象保证保留其原始状态/不变量,所有否则保持不变。换句话说,正确同步的发布没有必要以维持状态一致性。
在JSR-133 FAQ中,当他说:
你想确保所有其他线程(原文如此)正确地看到它
他没有提到不可变对象的状态。他的意思是您必须同步发布,以便另一个线程看到对不可变对象的引用。这两个语句所讨论的内容有细微差别:虽然JCIP指的是状态一致性,但FAQ页面指的是对不可变对象的引用的访问。
您提供的代码示例与Goetz在此处所说的内容完全无关,但为了回答您的问题,如果对象正确,正确初始化的final
字段将保持其预期值初始化(注意初始化和发布之间的区别)。代码示例还会同步对locations
字段的访问,以确保final
字段的更新是线程安全的。
事实上,为了进一步阐述,我建议您查看JCIP列表3.13(VolatileCachedFactorizer
)。请注意,即使OneValueCache
是不可变的,它也存储在volatile
字段中。为了说明常见问题解答声明,如果没有VolatileCachedFactorizer
,volatile
将无法正常工作。 &#34;同步&#34;指的是使用volatile
字段以确保对其进行的更新对其他线程可见。
说明第一个JCIP语句的好方法是删除volatile
。在这种情况下,CachedFactorizer
不会起作用。考虑一下:如果一个线程设置了一个新的缓存值,但另一个线程试图读取该值并且该字段不是volatile
,该怎么办?读者可能看不到更新的OneValueCache
。但是,回想起Goetz引用了不可变对象的状态,如果读者线程碰巧看到存储在OneValueCache
的{{1}}的最新实例,那么该实例的状态将是可见的并且正确构建。
因此虽然可能会丢失cache
的更新,但如果它被读取,则不可能会丢失cache
的状态,因为它是不可变的。我建议阅读附带的文字,说明&#34;易变参考用于确保及时可见性。&#34;
作为最后一个例子,考虑 a singleton that uses FinalWrapper
for thread safety。请注意,FinalWrapper实际上是不可变的(取决于单例是否可变),并且OneValueCache
字段实际上是非易失性的。回顾第二个FAQ语句,访问引用需要同步,这怎么能纠正&#34;实施可能是正确的!?
事实上, 可以在这里执行此操作,因为线程无需立即查看helperWrapper
的最新值。如果helperWrapper
保存的值非空,那就太好了!我们的第一个JCIP语句保证helperWrapper
的状态是一致的,并且我们有一个完全初始化的FinalWrapper
单例,可以很容易地返回。如果该值实际为null,则有两种可能性:首先,它可能是第一次调用并且尚未初始化;其次,它可能只是一个陈旧的价值。
如果是第一次调用,则会在同步上下文中再次检查字段本身,如第二个FAQ语句所示。它会发现此值仍为null,并将初始化新的Foo
并以同步方式发布。
如果它只是一个陈旧的值,通过输入synchronized块,线程可以设置一个先前发生的顺序,前面的字段写入。根据定义,如果某个值是陈旧的,那么某些编写器已经写入FinalWrapper
字段,并且当前线程尚未看到它。通过进入同步块,与之前的写入建立了先发生关系,因为根据我们的第一个场景,真正未初始化的helperWrapper
将由同一个锁初始化。因此,一旦方法进入同步上下文并获取最新的非空值,它就可以通过重新读取来恢复。
我希望我的解释和随附的例子能为你解决问题。