以下代码是否是线程安全的?

时间:2015-07-27 21:50:13

标签: java multithreading concurrency thread-safety

以下代码使用双重检查模式初始化变量。我相信代码是线程安全的,因为即使两个线程同时进入getMap()方法,映射也不会部分分配。所以我不必将地图也变得不稳定。推理是否正确?注意:初始化后,映射是不可变的。

class A {

    private Map<String, Integer> map;
    private final Object lock = new Object();

    public static Map<String, Integer> prepareMap() {
        Map<String, Integer> map = new HashMap<>();
        map.put("test", 1);
        return map;
    }

    public Map<String, Integer> getMap() {
        if (map == null) {
            synchronized (lock) {
                if (map == null) {
                    map = prepareMap();
                }
            }
        }
        return map;
    }
}

6 个答案:

答案 0 :(得分:4)

根据Java世界中的顶级名称,不是它不是线程安全的。您可以在此处阅读原因:http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html

最好使用ConcurrentHashmap或同步Map。

http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ConcurrentHashMap.html

编辑:如果您只想使地图线程的初始化安全(以便不会意外创建两个或多个地图),那么您可以做两件事。 1)声明地图时初始化地图。 2)使getMap()方法同步。

答案 1 :(得分:1)

不,你的推理是错误的,对地图的访问不是线程安全的,因为初始化后调用getMap()的线程可能不会调用synchronized(lock),因此不在happens-before relation到其他线程。

地图必须为volatile

答案 2 :(得分:1)

可以通过内联到

来优化代码
  public Map<String,Integer> getMap()
  {
     if(map == null)
     {
        synchronized(lock)
        {
           if(map == null)
           {
               map = new HashMap<>();  // partial map exposed
               map.put("test", 1);               
           }
        }
     }
     return map;
  }
}

在并发读写下使用HashMap非常危险,不要这样做。
Google HashMap无限循环

解决方案 -

synchronized展开到整个方法,以便读取map变量也处于锁定状态。这有点贵。

map声明为volatile,以防止重新排序优化。这很简单,也很便宜。

使用不可变的地图。 final字段也会阻止暴露部分对象状态。在您的特定示例中,我们可以使用Collections.singletonMap。但是对于包含更多条目的地图,我不确定JDK是否有公共实现。

答案 3 :(得分:1)

这只是事情可能出错的一个例子。要完全理解这些问题,我们无法替代The "Double-Checked Locking is Broken" Declaration中引用的prior answer

要获得接近完整风味的任何内容,请考虑两个处理器A和B,每个处理器都有自己的缓存,以及它们共享的主内存。

假设在处理器A上运行的线程A首先调用getMap。它在synchronized块内执行多​​项赋值。假设在线程A到达同步块的末尾之前,首先将map的赋值写入主内存。

同时,在处理器B上,线程B也调用getMap,并且恰好没有在其缓存中具有代表map的内存位置。它出现在主内存中以获取它,并且它的读取恰好在线程A分配给map之后命中,因此它看到非空map。线程B不进入同步块。

此时,线程B可以继续尝试使用HashMap,尽管线程A在创建它时的工作尚未写入主存储器。由于之前的使用,线程B甚至可能在其缓存中有map指向的内存。

如果您想尝试解决此问题,请参考引用文章中的以下引用:

  

有很多原因它不起作用。前几个原因   我们所描述的更为明显。在理解了这些之后,你可能会   试图设法找到一种方法来修复&#34;双重检查锁定   成语。你的修复不起作用:有更微妙的原因   你的修复工作没有成功。了解这些原因,想出一个更好的   修复,它仍然没有工作,因为有更微妙的   的原因。

这个答案只包含一个最明显的原因。

答案 4 :(得分:1)

不,它不是线程安全的。

基本原因是您可以重新排序您在Java代码中看不到的操作。让我们想象一个类似的模式,更简单的类:

class Simple {
    int value = 42;
}

在类似的getSimple()方法中,您指定/* non-volatile */ simple = new Simple ()。这里发生了什么?

  1. JVM为新对象分配一些空间
  2. JVM将此空间的一些部分设置为42(对于value
  3. JVM返回此空间的地址,然后将其分配给space
  4. 如果没有同步指令禁止它,可以重新排序这些指令。特别是,可以对步骤2和3进行排序,以便simple在构造函数完成之前获取新对象的地址!如果另一个线程然后读取simple.value,它将看到值0(字段的默认值)而不是42.这称为查看部分构造的对象。是的,这很奇怪;是的,我已经看到过这样的事情。这是一个真正的错误。

    你可以想象如果对象是一个非平凡的对象,比如HashMap,问题就更糟了;还有更多的操作,以及更奇怪的订购的可能性。

    将字段标记为volatile是告诉JVM的一种方式,&#34;从该字段读取值的任何线程也必须读取在写入该值之前发生的所有操作。&#34;这禁止那些奇怪的重新排序,这可以保证你能看到完整构造的物体。

答案 5 :(得分:-2)

除非您将lock声明为volatile,否则此代码可能会转换为非线程安全的字节码。

编译器可以优化表达式map == null,缓存表达式的值,从而只读取map属性一次。

volatile Map<> map指示Java VM在访问属性时始终读取属性map。 Thsi会禁止编译器进行这样的优化。

请参阅JLS Chapter 17. Threads and Locks