为什么ConcurrentHashMap适用于双重检查锁定?

3
在《Java并发编程实战》一书中提到,以下代码不具备线程安全性:
@NotThreadSafe
public class DoubleCheckedLocking {

    private static Resource resource;

    public static Resource getInstance(){
        if(resource == null){
            synchronized (DoubleCheckedLocking.class){
                if(resource == null)
                    resource = new Resource();
            }
        }
        return resource;
    }
}

由于以下原因,它不是线程安全的: - 一个线程可以创建新的Resource实例 - 另一个线程在“if”条件中同时可能得到非空引用,但Resource对象可能没有完全初始化

在这个问题中有类似的代码。资源存储在ConcurrentHashMap中,并且人们说它是线程安全的。就像这样:

public class DoubleCheckedLocking2 {

    private static ConcurrentHashMap<String, ComplexObject> cache = new ConcurrentHashMap<String, ComplexObject>();

    public static ComplexObject getInstance(String key) {
        ComplexObject result = cache.get(key);
        if (result == null) {
            synchronized (DoubleCheckedLocking2.class) {
                ComplexObject currentValue = cache.get(key);
                if (currentValue == null) {
                    result = new ComplexObject();
                    cache.put(key, result);
                } else {
                    result = currentValue;
                }
            }
        }
        return result;
    }
}

为什么将值存储在ConcurrentHashMap中可以使代码线程安全?我认为,复杂对象可能不会完全初始化,这个“部分对象”将保存在映射中。其他线程将读取部分未完全初始化的对象。
我知道什么是“happens-before”,我已经分析了JDK 8.0_31中的代码,但我仍然不知道答案。
我知道像computeIfAbsent、putIfAbsent之类的函数,我知道可以以不同的方式编写此代码。我只想知道使此代码线程安全的详细信息。
1个回答

4

发生在之前实际上是关键。从map.put(key, object)到后续的map.get(key),存在一个发生在之前的边缘,因此您检索的对象至少与存储在映射中时一样新。


所以我们不能保证映射中存储的ComplexObject实例将被完全初始化。在put操作之后,其他线程是否可能获取未完全初始化的ComplexObject引用?如果是,则不是线程安全的。 - dpolaczanski
我的回答意味着它由于“happens before”边缘而是线程安全的。写入线程在将对象放入映射之前完成的所有操作都保证在读取线程获取对象后被观察到。这就是“happens before”关系的传递性的本质。也许您错过了“happens before”覆盖程序顺序的事实? - Marko Topolnik

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