对 Java 中的安全发布和可见性感到困惑,尤其是不可变对象

kat*_*ex7 4 concurrency multithreading visibility immutability thread-safety

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

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

现在,我在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 */ }
Run Code Online (Sandbox Code Playgroud)

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

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

小智 5

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

简而言之,Goetz 在链接的 JSR 133 FAQ 页面中的声明更加“正确”,尽管不是您所想的那样

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

在 JSR-133 FAQ 中,他说:

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

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

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

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

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

因此,尽管有可能丢失对 的更新cache,但不可能丢失OneValueCacheif 被读取的状态,因为它是不可变的。我建议阅读随附的文字,说明“用于确保及时可见性的易失性参考”。

作为最后一个示例,考虑用于线程安全的单例FinalWrapper。请注意,FinalWrapper 实际上是不可变的(取决于单例是否可变),并且该helperWrapper字段实际上是非易失性的。回想一下第二个常见问题解答声明,访问引用需要同步,这个“正确”的实现怎么可能是正确的!?

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

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

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

我希望我的解释和随附的示例能够帮助您弄清楚问题。