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

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了对Java中的安全发布和可见性感到困惑,尤其是使用Immutable Objects相关的知识,希望对你有一定的参考价值。

当我阅读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,但是另一个线程使用该类时字段位置为空,该怎么办?

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

答案

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

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

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

在JSR-133 FAQ中,当他说:

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

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

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

事实上,为了进一步阐述,我建议你看看JCIP上市3.13(VolatileCachedFactorizer)。请注意,即使OneValueCache是不可变的,它也存储在volatile字段中。为了说明FAQ语句,如果没有VolatileCachedFactorizervolatile将无法正常工作。 “同步”是指使用volatile字段以确保对其进行的更新对其他线程可见。

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

因此虽然有可能失去对cache的更新,但如果它被读取则不可能失去OneValueCache的状态,因为它是不可变的。我建议阅读附带的文字,说明“用于确保及时查看的易变参考”。

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

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

如果是第一次调用,则在同步上下文中再次检查字段本身,如第二个FAQ语句所示。它会发现此值仍为null,并将初始化新的FinalWrapper并使用同步发布。

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

我希望我的解释和我所提供的相应例子能为你解决问题。

以上是关于对Java中的安全发布和可见性感到困惑,尤其是使用Immutable Objects的主要内容,如果未能解决你的问题,请参考以下文章

php手动可见性示例混淆

Java线程安全:可见性,原子性,有序性

Java多线程安全可见性和有序性之Volatile

对使用 Spring 和 Hibernate 的 Java 日志系统感到困惑

对JAVA界面感到困惑[重复]

volatile为啥不能保证原子性