multithreading - 对Java中的安全发布和可见性(尤其是不可变对象(immutable对象))感到困惑

标签 multithreading concurrency thread-safety visibility immutability

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

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

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

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

我还读到,如果所有字段都是最终字段,那么即使对象不是说不可变的,我们也可以确保安全发布。
例如,由于这种保证,我一直认为,在发布此类的对象时,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

最佳答案

这个问题已经被回答过几次了,但是我觉得其中很多回答是不够的。看:

  • https://stackoverflow.com/a/14617582
  • https://stackoverflow.com/a/35169705
  • https://stackoverflow.com/a/7887675
  • Effectively Immutable Object
  • 等...

  • 简而言之,在链接的JSR 133 FAQ页面中,Goetz的声明更“正确”,即,尽管不像您在思考那样。

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

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

    you want to ensure that it is seen correctly by all of the other thread (sic)



    他不是在指不可变对象(immutable对象)的状态。他的意思是您必须同步发布才能使另一个线程看到对不可变对象(immutable对象)的引用。这两个语句所讨论的内容之间有微妙的区别:JCIP指的是状态一致性,而FAQ页面则指的是对不可变对象(immutable对象)的引用的访问。

    您提供的代码示例实际上与Goetz在此处所说的内容没有任何关系,但是要回答您的问题,如果对象正确地初始化了,则正确初始化的final字段将保留其期望值(请注意初始化和初始化之间的区别)出版物)。该代码示例还同步了对locations字段的访问,以确保对final字段的更新是线程安全的。

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

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

    因此,尽管有可能丢失对cache的更新,但如果是不可更改的,则不可能丢失OneValueCache的状态(如果已读取)。我建议阅读随附的文字,其中指出“可变引用用于确保及时可见。”

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

    实际上,这里可以执行此操作,因为线程不必立即查看helperWrapper的最新值。如果helperWrapper保留的值不为null,那就太好了!我们的第一个JCIP语句保证FinalWrapper的状态是一致的,并且我们有一个完全初始化的Foo单例,可以很容易地返回它。如果该值实际上为null,则有两种可能性:首先,有可能是第一个调用,并且尚未初始化。其次,这可能只是一个过时的值(value)。

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

    在它只是一个过时的值的情况下,通过输入同步块(synchronized block),线程可以设置事前先后顺序,并先写入该字段。根据定义,如果值是陈旧的,则表明某个编写器已经将其写入helperWrapper字段,并且当前线程尚未看到它。通过进入同步块(synchronized block),将与之前的写入建立先发生后关系,因为根据我们的第一种情况,真正的未初始化helperWrapper将由相同的锁初始化。因此,一旦方法进入同步上下文并获得最新的非空值,它可以通过重新读取来恢复。

    我希望我的解释和随附的示例可以帮助您解决问题。

    关于multithreading - 对Java中的安全发布和可见性(尤其是不可变对象(immutable对象))感到困惑,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47957697/

    相关文章:

    c# - 线程安全类是否应该在其构造函数的末尾设置内存屏障?

    Android - 线程正在破坏应用程序

    C++/g++ : Concurrent program

    C#变量线程安全

    c++ - make_pair 是原子的吗?

    c - 如何让 Posix 线程在一定时间后完成其工作?

    Java动态方法线程产生

    c++ - 在大型并发数组中组织已用和未用元素的有效方法

    Android:在 getView() 中执行异步操作的最佳实践

    java - Thread 中的 join() 方法是否保证完美工作,还是也依赖于各个 JVM?