30

我已经阅读了很多,但还没有找到明确的答案。

我有一个看起来像这样的类:

    public class Foo() {

        private static final HashMap<String, HashMap> sharedData;

        private final HashMap myRefOfInnerHashMap;

        static {
           // time-consuming initialization of sharedData
           final HashMap<String, String> innerMap = new HashMap<String, String>;
           innerMap.put...
           innerMap.put...
           ...a

           sharedData.put(someKey, java.util.Collections.unmodifiableMap(innerMap));
        }

        public Foo(String key) {
            this.myRefOfInnerHashMap = sharedData.get(key);
        }

        public void doSomethingUseful() {
            // iterate over copy
            for (Map.Entry<String, String> entry : this.myRefOfInnerHashMap.entrySet()) {
                ...
            }
        }
     }

而且我想知道从 Foo 的实例访问 sharedData 是否是线程安全的(如构造函数和 doSomethingUseful() 中所示)。Foo 的许多实例将在多线程环境中创建。

我的意图是 sharedData 在静态初始化程序中初始化,之后不再修改(只读)。

我读到的是不可变对象本质上是线程安全的。但我只在似乎是实例变量的上下文中看到了这一点。不可变的静态变量是线程安全的吗?

我发现的另一个构造是 ConcurrentHashMap。我可以制作 ConcurrentHashMap 类型的 sharedData,但它包含的 HashMaps 也必须是 ConcurrentHashMap 类型吗?基本上..

private static final ConcurrentHashMap<String, HashMap> sharedData;

或者

private static final ConcurrentHashMap<String, ConcurrentHashMap> sharedData;

还是会更安全(但简单地克隆()成本更高)?

this.myCopyOfData = sharedData.get(key).clone();

TIA。

(静态初始化器已被编辑以提供更多上下文。)

4

8 回答 8

28

final的引用sharedData线程安全的,因为它永远无法更改。Map 的内容不是线程安全的,因为它需要最好用 GuavaImmutableMap实现包装,java.util.Collections.unmodifiableMap()或者使用包中的 Map 实现之一java.util.concurrent

只有两者都做,您才能在Map上获得全面的线程安全性。任何包含的 Map 都需要是不可变的,或者是并发实现之一。

.clone() 从根本上被破坏了,远离

默认情况下克隆是浅克隆,它只会返回对容器对象的引用而不是完整的副本。在普遍可用的信息中详细记录了原因。

于 2010-03-09T20:05:00.093 回答
8

静态初始化块中静态最终字段的初始化是线程安全的。但是,请记住,静态最终引用指向的对象可能不是线程安全的。如果您引用的对象是线程安全的(例如,它是不可变的),那么您就清楚了。

除非您按照问题中的建议使用 ConcurrentHashMap ,否则不能保证外部 HashMap 中包含的每个单独的 HashMap 都是线程安全的。如果不使用线程安全的内部 HashMap 实现,当两个线程访问同一个内部 HashMap 时,可能会得到意想不到的结果。请记住,只有对 ConcurrentHashMap 的一些操作是同步的。例如,迭代不是线程安全的。

于 2010-03-09T20:04:22.543 回答
6

是的,这也是线程安全的。在允许任何线程访问它们之前,将初始化静态类的所有最终成员。

如果static块在初始化期间失败,ExceptionInInitializerError将在第一次尝试初始化的线程中引发。随后尝试引用该类将引发NoClassDefFoundError.

通常,a 的内容HashMap不能保证跨线程的可见性。但是,类初始化代码使用synchronized块来防止多个线程初始化类。此同步将刷新映射(及其HashMap包含的实例)的状态,以便它们对所有线程正确可见 - 假设在类初始化程序之外没有对映射或它包含的映射进行任何更改。

有关类初始化和同步要求的信息,请参阅Java 语言规范第 12.4.2 节。

于 2010-03-09T20:01:59.570 回答
6

什么是线程安全的?当然,HashMap 的初始化是线程安全的,因为所有 Foo 共享同一个 Map 实例,并且保证 Map 存在,除非在静态初始化中发生异常。

但是修改 Map 的内容肯定不是线程安全的。static final 意味着 Map sharedData 不能切换到另一个 Map。但是地图的内容是一个不同的问题。如果同一时间多次使用给定密钥,您可能会遇到并发问题。

于 2010-03-09T20:08:31.350 回答
5

不,除非它们是不可变的。

他们唯一做的就是

  • 达到班级水平
  • 避免更改引用。

不过,如果您的属性是可变的,那么它不是线程安全的。

另请参阅:我们是否同步最终的实例变量?

除了它们是类级别之外,它完全相同。

于 2010-03-09T20:16:11.517 回答
3

final static变量本身没有什么线程安全的。声明一个成员变量final static只能确保这个变量只分配一次。

线程安全问题与您如何声明变量关系不大,而是取决于您如何与变量交互。因此,如果没有有关您的程序的更多详细信息,就不可能真正回答您的问题:

  • 多个线程是否会修改sharedData变量的状态?
  • 如果是这样,您是否同步所有的写入(读取)sharedData

使用 ConcurrentHashMap 仅保证 的各个方法Map是线程安全的,它不会进行诸如以下线程安全的操作:

if (!map.containsKey("foo")) {
    map.put("foo", bar);
}
于 2010-03-09T20:04:06.943 回答
1

您实际上不是在问静态初始化是否sharedData是线程安全的并且只执行一次吗?

是的,就是这样。

当然这里很多人都正确指出了,内容sharedData还是可以修改的。

于 2010-03-09T20:32:32.553 回答
0

在这种情况下,只有 sharedData 对象是不可变的,这意味着您将始终使用同一个对象。但是其中的任何数据都可以随时从任何线程更改(删除、添加等)。

于 2010-03-09T20:04:07.300 回答