对Java中的安全发布和可见性感到困惑,尤其是使用Immutable Objects

时间:2017-12-24 02:49:34

标签: multithreading concurrency thread-safety visibility immutability

当我阅读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

1 个答案:

答案 0 :(得分:3)

这个问题之前已经回答了几次,但我觉得很多答案都不合适。参见:

简而言之,Goetz在链接的JSR 133常见问题解答页面中的陈述更多&#34;正确&#34;,虽然不是您想的方式

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

在JSR-133 FAQ中,当他说:

  

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

他没有提到不可变对象的状态。他的意思是您必须同步发布,以便另一个线程看到对不可变对象的引用。这两个语句所讨论的内容有细微差别:虽然JCIP指的是状态一致性,但FAQ页面指的是对不可变对象的引用的访问。

您提供的代码示例与Goetz在此处所说的内容完全无关,但为了回答您的问题,如果对象正确,正确初始化的final字段将保持其预期值初始化(注意初始化和发布之间的区别)。代码示例还会同步对locations字段的访问,以确保final字段的更新是线程安全的。

事实上,为了进一步阐述,我建议您查看JCIP列表3.13(VolatileCachedFactorizer)。请注意,即使OneValueCache是不可变的,它也存储在volatile字段中。为了说明常见问题解答声明,如果没有VolatileCachedFactorizervolatile 将无法正常工作。 &#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将由同一个锁初始化。因此,一旦方法进入同步上下文并获取最新的非空值,它就可以通过重新读取来恢复。

我希望我的解释和随附的例子能为你解决问题。