【问题标题】:Confused about safe publishing and visibility in Java, especially with Immutable Objects对 Java 中的安全发布和可见性感到困惑,尤其是不可变对象
【发布时间】:2017-12-24 02:49:34
【问题描述】:

当我阅读 Brian Goetz 的 Java Concurrency in Practice 时,我记得他在关于可见性的章节中说过“另一方面,即使不使用同步来发布对象引用,也可以安全地访问不可变对象”。

我认为这意味着如果您发布不可变对象,则所有字段(包括可变最终引用)对可能使用它们的其他线程都是可见的,并且至少在该对象完成构造时是最新的。

现在,我在https://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html 中读到了 “现在,说了这么多,如果在一个线程构造了一个不可变对象(即一个只包含最终字段的对象)之后,你想确保它被所有其他线程正确地看到,你仍然通常需要使用同步。没有其他方法可以确保,例如,对不可变对象的引用将被第二个线程看到。程序从 final 字段中获得的保证应该通过对如何在您的代码中管理并发。”

它们似乎相互矛盾,我不确定该相信哪个。

我还读到如果所有字段都是最终的,那么即使对象不是不可变的,我们也可以确保安全发布。 比如我一直认为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 */ }

例如,在此代码示例中,如果最终保证为假,并且线程创建了此类的实例,然后对该对象的引用不为空,但字段位置在另一个线程使用时为空,该怎么办?那个班?

再一次,我不知道哪个是正确的,或者我是否碰巧误解了文章或 Goetz

【问题讨论】:

    标签: multithreading concurrency thread-safety visibility immutability


    【解决方案1】:

    这个问题之前已经回答过几次了,但我觉得很多答案都不够充分。见:

    简而言之,Goetz 在链接的 JSR 133 FAQ 页面中的陈述更“正确”,虽然不是您想的那样

    当 Goetz 说不可变对象即使在没有同步的情况下发布时也可以安全使用时,他的意思是说,对不同线程可见的不可变对象可以保证保留其原始状态/不变量,所有否则保持不变。换句话说,正确同步的发布对于保持状态一致性不是必需的

    在 JSR-133 常见问题解答中,当他这样说时:

    你想确保它被所有其他线程正确看到(原文如此)

    他指的不是不可变对象的状态。他的意思是您必须同步发布才能让另一个线程看到对不可变对象的引用。这两个语句所讨论的内容存在细微差别:JCIP 指的是状态一致性,而 FAQ 页面指的是对不可变对象的引用的访问。

    您提供的代码示例实际上与 Goetz 在这里所说的任何内容无关,但要回答您的问题,如果对象正确初始化final 字段将保持其预期值/strong>(注意初始化和发布之间的区别)。该代码示例还同步对locations 字段的访问,以确保对final 字段的更新是线程安全的。

    事实上,为了进一步详细说明,我建议您查看 JCIP 清单 3.13 (VolatileCachedFactorizer)。请注意,即使OneValueCache 是不可变的,它也存储在volatile 字段中。为了说明常见问题解答声明,如果没有volatileVolatileCachedFactorizer 将无法正常工作。 “同步”是指使用volatile 字段以确保对其进行的更新对其他线程可见。

    说明第一个 JCIP 语句的一个好方法是删除 volatile。在这种情况下,CachedFactorizer 将不起作用。考虑一下:如果一个线程设置了一个新的缓存值,但另一个线程试图读取该值并且该字段不是volatile,该怎么办?读者可能看不到更新后的OneValueCache。但是,回想一下 Goetz 指的是不可变对象的 state,如果阅读器线程碰巧看到 OneValueCache 的最新实例存储在 cache,那么它的状态实例将是可见的并且构造正确。

    因此,虽然cache 的更新可能会丢失,但如果OneValueCache 被读取,则不可能丢失它的状态,因为它是不可变的。我建议阅读随附的文字说明“用于确保及时可见性的可变引用。”

    作为最后一个例子,考虑 a singleton that uses FinalWrapper for thread safety。请注意,FinalWrapper 实际上是不可变的(取决于单例是否可变),并且 helperWrapper 字段实际上是非易失性的。回顾第二个FAQ语句,访问引用需要同步,这个“正确”的实现怎么可能是正确的!?

    事实上,这里可以这样做的,因为线程没有必要立即看到helperWrapper 的最新值。如果helperWrapper 持有的值不为空,那就太好了!我们的第一个 JCIP 语句保证 FinalWrapper 的状态是一致的,并且我们有一个完全初始化的 Foo 单例,可以很容易地返回。如果该值实际为null,则有2种可能:第一,有可能是第一次调用,还没有初始化;其次,它可能只是一个陈旧的值。

    如果是第一次调用,则在同步上下文中再次检查字段本身,如第二个常见问题解答语句所建议的那样。会发现这个值还是null,会初始化一个新的FinalWrapper,同步发布。

    在它只是一个陈旧值的情况下,通过进入同步块,线程可以设置一个先发生的顺序,并在前写入该字段。根据定义,如果一个值是陈旧的,那么某个作者已经写入了helperWrapper 字段,并且当前线程还没有看到它。通过进入同步块,与之前的写入建立了发生前的关系,因为根据我们的第一个场景,一个真正未初始化的helperWrapper将被同一个锁初始化。因此,一旦方法进入同步上下文,它就可以通过重新读取来恢复,并获得最新的非空值。

    我希望我的解释和我所提供的随附示例能够为您解决问题。

    【讨论】:

    • 非常感谢您解决这个问题。当你说“如果对象被正确初始化,正确初始化的最终字段将保持其预期值”时,为了 100% 的自信,你的意思是那些正确构造的具有所有 finale 字段的对象可以安全地发布,因为它们保持了预期价值,因为它们是正确构造的吗?
    • 是的,它们保证在构造过程中具有初始化的值。
    • 初始化并且在共享该对象时可见吗?
    • 是的。再次回忆一下VolatileCachedFactorizer - 您仍然需要确保安全地共享 reference 以便及时查看它,但除此之外,final 字段保证会被初始化 并且 可见。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-02-25
    • 1970-01-01
    • 1970-01-01
    • 2012-01-11
    • 2011-10-12
    相关资源
    最近更新 更多