20

谁能告诉我这个类是否是线程安全的?

class Foo {

    private final Map<String,String> aMap;

    public Foo() {
        aMap = new HashMap<String, String>();
        aMap.put("1", "a");
        aMap.put("2", "b");
        aMap.put("3", "c");
    }

    public String get(String key) {
        return aMap.get(key);
    }

}

编辑:我的错没有澄清这个问题。根据JMM 常见问题解答

应该提供初始化安全的新保证。如果一个对象被正确构造(这意味着在构造过程中对它的引用不会逃逸),那么所有看到对该对象引用的线程也将看到在构造函数中设置的其最终字段的值,而无需同步。

这让我混淆了 aMap 的集合是aMap = new HashMap<String, String>();. 所以其他线程可以看到这些

aMap.put("1", "a");
aMap.put("2", "b");
aMap.put("3", "c");

或不 ?

编辑:我发现这个问题完全符合我的问题

4

9 回答 9

22

正如已经指出的那样,它绝对是线程安全的,并且final由于其内存可见性效应而在这里很重要。

存在final保证其他线程将在构造函数完成后在没有任何外部同步的情况下看到映射中的值。没有final它就不能保证在所有情况下,并且在使新构造的对象可用于其他线程时,您需要使用安全的发布习惯用法,即(来自Java Concurrency in Practice):

  • 从静态初始化器初始化对象引用;
  • 将对它的引用存储到 volatile 字段或 AtomicReference 中;
  • 将对它的引用存储到正确构造的对象的最终字段中;或者
  • 将对它的引用存储到由锁正确保护的字段中。
于 2011-06-23T16:13:53.710 回答
8

是的。没有办法修改引用aMap本身,或者在构造函数之后添加到地图中(除非反射)。

如果你暴露aMap它就不会,因为两个线程可以同时修改映射。

您可以通过Collections.unmodifiableCollectionCollections.unmodifiableMapaMap使您的课程不可修改。

于 2011-06-23T16:03:48.673 回答
3

Guava具有不可变的类,可以使这种事情变得更容易并保证不可变:

private final ImmutableMap<String, String> aMap = ImmutableMap.of(
    "1", "a",
    "2", "b",
    "3", "c");
于 2011-06-23T16:09:02.220 回答
2

是的,只要这是整个类定义而不是其中的一个片段

关键事实是,在aMap构建后无法修改其内容。

于 2011-06-23T16:03:14.700 回答
1

此类没有并发问题,因为您只公开了一个 get 方法。如果您添加一些修改地图的方法,您必须将此方法标记为synchronized.

于 2011-06-23T16:06:46.107 回答
0

现在它应该是线程安全的。但是,如果您添加其他修改哈希图的方法,则不会。

于 2011-06-23T16:05:09.907 回答
0

我不认为上面的代码片段是线程安全的。代码安全的唯一行是

aMap = new HashMap<String, String>();

根据http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html中给出的示例,

class FinalFieldExample {
    final int x;
    int y;
    static FinalFieldExample f;
    public FinalFieldExample() {
      x = 3;
      y = 4;
    }

    static void writer() {
       f = new FinalFieldExample();
    }

   static void reader() {
     if (f != null) {
       int i = f.x; // x is guaranteed to be 3
       int j = f.y; // y can have any value 
     }
   }
}

这意味着一旦最终字段被初始化,就无法保证线程安全。因为只有引用分配才能保证是线程安全的,并且根据您的示例,对象本身可以是可变的。以下语句可能不是线程安全的

aMap.put("1", "a");
aMap.put("2", "b");
aMap.put("3", "c");

编辑我的坏事后来看到了代码下面的评论

查看正确构造的字段值的能力很好,但如果字段本身是一个引用,那么您还希望您的代码查看它指向的对象(或数组)的最新值。如果您的字段是 final 字段,这也是有保证的。因此,您可以拥有一个指向数组的最终指针,而不必担心其他线程会看到数组引用的正确值,但会看到数组内容的错误值。同样,这里的“正确”是指“在对象的构造函数结束时是最新的”,而不是“可用的最新值”。

于 2016-04-25T05:11:08.287 回答
0

这是很久以前问的问题。不过,我决定回答这个问题。首先,它是完全线程安全的代码。原因如下。

  1. 它的状态被很好地封装。类的状态由映射及其键值对组成,这些键值对是不可变的字符串。因此,通过封装,类以线程状态的方式与其他线程共享其状态。

  2. 只有公共方法 Foo 类提供了get发布不可变对象的方法,这些对象对于并行线程是安全的,因为它们不能修改已发布的对象。

  3. map 变量final使其不可变并提供内存可见性。

要回答这个问题,它实际上是安全初始化。没有物体在逃逸。其他线程看不到。

于 2020-01-13T21:07:21.277 回答
0

正如 16.3 节的 Java Concurrency in Practice 中所描述的,这必须是线程安全的。

初始化安全保证对于正确构造的 对象,所有线程都将看到构造函数设置的最终字段的正确值,而不管对象是如何发布的。此外,可以通过正确构造的对象的 final 字段(例如 final 数组的元素或 final 字段引用的 HashMap 的内容)到达的任何变量也保证对其他线程可见

对于具有最终字段的对象,初始化安全禁止使用对该对象的引用的初始加载来重新排序构造的任何部分。当构造函数完成时,对构造函数进行的最终字段以及可通过这些字段访问的任何变量的所有写入都将被“冻结”,并且任何获得对该对象的引用的线程都可以保证看到一个至少为与冻结值一样最新。初始化可通过最终字段访问的变量的写入不会在构建后冻结之后的操作中重新排序

该部分的示例:

 @ThreadSafe
 public class SafeStates {

    private final Map<String, String> states;

    public SafeStates() {
        states = new HashMap<String, String>(); states.put("alaska", "AK");
        states.put("alabama", "AL");
        ...
        states.put("wyoming", "WY");
     }

     public String getAbbreviation(String s) { 
        return states.get(s);
     } 
 }
于 2020-11-11T17:27:11.393 回答