Java.util.concurrent集合的安全发布

6
这段代码中,volatile是否多余?
public class Test {
    private volatile Map<String, String> map = null;

    public void resetMap() { map = new ConcurrentHashMap<>(); }

    public Map<String, String> getMap() { return map; }
}

换句话说,map = new ConcurrentHashMap<>(); 是否提供任何可见性保证?
据我所见,ConcurrentMap 提供的唯一保证是:
在将对象作为键或值放入ConcurrentMap之前,线程中的操作发生在在另一个线程中访问或删除该对象后的操作之前。
那么java.util.concurrent中的其他线程安全集合(CopyOnWriteArrayList等)呢?
6个回答

8

volatile并不是多余的,因为你正在改变对映射的引用。也就是说,ConcurrentMap仅提供有关集合内容而非引用的保证。

另一种替代方法是:

public class Test {
    private final Map<String, String> map = new ConcurrentHashMap<>();

    public void resetMap() { map.clear(); }

    public Map<String, String> getMap() { return map; }
}

Java.util.concurrent中的其他线程安全集合(CopyOnWriteArrayList等)怎么样?

只有集合的行为是线程安全的。对集合的引用不是线程安全的,将元素添加到集合中并不能使它们变得线程安全。


6
在这里需要使用volatile。它适用于引用,而不是引用到的内容。换句话说,对象具有线程安全性并不重要,其他线程将无法看到map字段的新值(例如,可能会看到先前引用的并发映射或null)。

此外,即使您的对象是不可变的(例如String),您仍然需要使用volatile,更不用说其他线程安全集合,如CopyOnWriteArrayList


1
不确定为什么,但我认为因为构造函数可能使用某种内存屏障,它也会提供一些可见性保证 - 除了赋值在构造函数之外完成! - assylias

4

这不仅仅涉及到引用问题。通常情况下,如果没有 volatile 修饰符,其他线程可能会观察到一个对象的新引用,但是会以部分构造状态观察到该对象。一般来说,即使查阅文档后,也很难知道哪些对象可以安全地通过数据竞争进行发布。有趣的是,JLS 对于 线程安全的不可变对象 进行了保证,因此如果文档提到这两个属性,应该就足够了。

ConcurrentHashMap 显然不是不可变对象,所以这并不适用,并且文档也没有提到任何关于通过数据竞争进行发布的内容。通过细致地检查源代码,我们可以得出结论,它确实是安全的,但我不建议在没有清晰记录此属性的情况下依赖这样的发现。


我的想法是,第一次使用地图时,它将重新同步所有内容 - 这让我认为volatile没有提供额外的保证。部分构造的对象参数是一个强有力的论据。 - assylias
虽然我不确定ConcurrentHashMap是否可以部分构造,因为它可能在其构造函数中使用某种形式的同步。 - assylias
1
也许我们应该检查一下细节,但它们可能会变得非常混乱 :) - Marko Topolnik
我已经检查过了,这段代码让我感到非常可疑:for (int i = 0; i < this.segments.length; ++i) this.segments[i] = new Segment<K,V>(cap, loadFactor); 这个数组的填充没有任何保护措施。 - Marko Topolnik
哈,但他们在Java 7中修复了它!告诉过你这会变得混乱... :) - Marko Topolnik
显示剩余4条评论

0

好的 - 我能够构建一个例子来证明(在我的机器上:JDK 1.7.06 / Win 7 64位),如果该字段不是易失性的,它会出现错误 - 如果地图不是易失性的,则程序永远不会打印循环退出 - 如果地图是易失性的,则会打印循环退出。QED。

public class VolatileVisibility extends Thread {

    Map<String, String> stop = null;

    public static void main(String[] args) throws InterruptedException {
        VolatileVisibility t = new VolatileVisibility();
        t.start();
        Thread.sleep(100);
        t.stop = new ConcurrentHashMap<>(); //write of reference
        System.out.println("In main: " + t.stop); // read of reference
        System.out.println("Waiting for run to finish");
        Thread.sleep(200);
        System.out.println("Still waiting");
        t.stop.put("a", "b"); //write to the map
        Thread.sleep(200);
        System.exit(0);
    }

    public void run() {
        System.out.println("In run: " + stop); // read of reference
        while (stop == null) {
        }
        System.out.println("Loop exited");
    }
}

好的,但是在这里你只证明了引用可能永远不会被观察到。在数据竞争中发布时,这是常态。要证明您实际上可以观察到一个被撕裂的对象将更加困难。 - Marko Topolnik
@MarkoTopolnik 更新了一个放置到地图上的操作,但仍未同步任何内容。是的,展示一个部分构建的地图会更困难——甚至不确定如何做到这一点/是否在我的架构上可行。 - assylias
1
啊,这就是你想要的。但只要读取线程不观察ref本身,对映射的写入就不会强制执行任何操作。最容易看到这一点的方法是注意到stop == null在循环外被允许提升,编译成if (stop == null) while (true); - Marko Topolnik
所以你需要一个更复杂的场景,在这种情况下,尽管存在数据竞争,这种优化不会发生,然后需要......好吧,这变得非常混乱,很难理解。这就是为什么我们喜欢和拥抱volatile :) - Marko Topolnik
@MarkoTopolnik 只有在编译run时才会发生这种情况,所以最有可能的情况就是这样。 - assylias
对于 private 变量,我认为即使在字节码级别上 hoisting 也是允许的。 - Marko Topolnik

0

内存一致性属性

对于一个 volatile 字段的写操作 happens-before 于每个随后对该字段的读操作。volatile 字段的写入和读取具有类似于进入和退出监视器的内存一致性效果,但不涉及互斥锁定。

在线程将对象放入任何并发集合之前的操作 happens-before 于在另一个线程中访问或删除该元素的操作。


0

我的印象是Doug Lea的并发对象可以通过数据竞争进行安全发布,即使被误用也仍然是“线程安全”的。虽然他可能不会公开宣传这一点。


对我来说,最重要的是只要没有明确记录,执行此操作的代码就可以被视为不正确。 - Marko Topolnik

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接