从多个线程中获取java.util.HashMap的值是否安全(不进行修改)?

28 浏览
0 Comments

从多个线程中获取java.util.HashMap的值是否安全(不进行修改)?

有这样一种情况,需要构建一个地图,一旦初始化,就不会再进行修改。然而,它会被多个线程访问(只能通过get(key)访问)。在这种情况下,使用java.util.HashMap是否安全?\n(目前,我正在愉快地使用java.util.concurrent.ConcurrentHashMap,并且没有测量到需要改进性能的需求,但我只是好奇是否一个简单的HashMap就足够。因此,这个问题不是“我应该使用哪一个?”也不是一个性能问题。而是一个“是否安全?”的问题。)

0
0 Comments

从这些内容中可以得出以下结论:

1. 从同步的角度来看,从`java.util.HashMap`中获取值是安全的,即使多个线程同时进行读取操作也不会出现问题。

2. 但是从内存的角度来看,问题就有所不同。如果没有在当前线程中进行内存写入操作,那么其他线程可能无法看到HashMap的更新版本。

3. 在Java中,内存写入操作通常通过使用`synchronized`或`volatile`关键字,或者通过使用一些Java并发构造来实现。

4. Brian Goetz在他的文章中详细介绍了Java内存模型的相关内容。

5. 如果HashMap在正确初始化之前没有被任何线程看到,那么这个问题就不存在。

6. 对于只读数据结构来说,只要HashMap中存储的数据是不可变的,就可以安全地进行读取操作。

7. 使用`volatile`成员或者在构造函数中使用`final`成员可以确保`happens-before`关系,从而确保其他线程可以看到完全构建的HashMap。

要安全地从`java.util.HashMap`中获取值,可以通过使用`synchronized`或`volatile`关键字,或者在构造函数中使用`final`成员来确保内存的正确读取。此外,存储在HashMap中的数据应该是不可变的以确保数据的安全性。

0
0 Comments

在多线程环境下,从`java.util.HashMap`中获取值是安全的(不进行修改)吗?这个问题的出现是因为当构造线程将对`HashMap`的引用可见给其他线程时,只有在引用被安全发布的情况下才是安全的。与`HashMap`本身的内部工作机制无关,安全发布处理的是构造线程如何使对地图的引用对其他线程可见。

基本上,这里唯一可能出现的竞争是在构造`HashMap`和任何可能在其完全构造之前访问它的读取线程之间。大部分讨论都是关于地图对象的状态会发生什么变化,但这是无关紧要的,因为您从不修改它 - 因此唯一有趣的部分是如何发布`HashMap`引用。

例如,想象一下您这样发布地图:

class SomeClass {
   public static HashMap MAP;
   public synchronized static setMap(HashMap m) {
     MAP = m;
   }
}

...在某些时刻调用了`setMap()`,其他线程正在使用`SomeClass.MAP`来访问地图,并像这样检查是否为null:

HashMap map = SomeClass.MAP;
if (map != null) {
  .. 使用地图
} else {
  .. 默认行为
}

即使它看起来可能是安全的,但实际上并不安全。问题在于`SomeObject.MAP`的设置和后续读取之间没有`happens-before`关系,因此读取线程可以看到一个部分构造的地图。这几乎可以做任何事情,甚至在实践中会导致读取线程进入无限循环。

要安全地发布地图,您需要在对`HashMap`的引用的写入(即发布)和对该引用的后续读取(即消费)之间建立一个`happens-before`关系。方便的是,只有几种容易记住的方法可以实现这一点:

1. 通过正确加锁的字段交换引用(JLS 17.4.5)。

2. 使用静态初始化程序执行初始化存储(JLS 12.4)。

3. 通过`volatile`字段交换引用(JLS 17.4.5),或作为此规则的结果,通过`AtomicX`类交换引用。

4. 将值初始化为`final`字段(JLS 17.5)。

对于您的场景,最有趣的方法是(2),(3)和(4)。特别是,(3)直接适用于上面的代码:如果将`MAP`的声明转换为:

public static volatile HashMap MAP;

那么一切都是合法的:看到非空值的读取者必然与对`MAP`的存储之间有一个`happens-before`关系,因此可以看到与地图初始化相关联的所有存储。

其他方法会改变方法的语义,因为(2)(使用静态初始化程序)和(4)(使用`final`)意味着您无法在运行时动态设置`MAP`。如果您不需要在运行时动态设置它,则只需将`MAP`声明为`static final HashMap<>`即可确保安全发布。

实际上,对于安全访问“从不修改的对象”,规则很简单:

- 如果要发布的对象不是固有不可变的(即所有字段都声明为`final`),并且:

- 您已经可以在声明时分配将被分配的对象:只需使用`final`字段(包括对静态成员的`static final`)。

- 您希望在引用已经可见之后再分配对象:使用`volatile`字段。

就是这样!

实际上,这非常高效。例如,使用`static final`字段允许JVM假设该值在程序的整个生命周期中都不会更改,并对其进行大量优化。使用`final`成员字段允许大多数架构以与普通字段读取等效的方式读取字段,并且不会阻碍进一步优化。

最后,使用`volatile`确实会产生一些影响:在许多架构上不需要硬件屏障(例如x86,特别是不允许读取通过读取的架构),但某些优化和重排序可能无法在编译时发生 - 但这种影响通常很小。作为交换,您实际上得到了比您要求的更多 - 不仅可以安全发布一个`HashMap`,还可以将任意数量的不可修改的`HashMap`存储到相同的引用中,并确保所有读取者都会看到一个经过安全发布的地图。

有关更多详细信息,请参考`Shipilev`的文章或`Manson`和`Goetz`的[FAQ](https://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#finalRight)。

[1] 引用自`shipilev`。

a 这听起来很复杂,但我的意思是您可以在构造时分配引用 - 可以在声明点或构造函数(成员字段)或静态初始化程序(静态字段)中进行。

b 可选地,您可以使用`synchronized`方法进行获取/设置,或者使用`AtomicReference`等,但我们只考虑您可以做的最小工作。

c 一些具有非常弱内存模型的架构(我正在看着你,Alpha)可能需要在`final`读取之前进行某种读屏障 - 但这些在今天非常罕见。

“`never modify` `HashMap`”并不意味着`HashMap`的`state of the map object`是线程安全的。我认为只有上述官方文档未明确指出其线程安全性时,才会出现库实现的灰色区域。

- 没错,在某些情况下确实存在灰色区域:当我们说“修改”时,我们实际上指的是任何内部执行某些可能与其他线程的读取或写入竞争的写操作。这些写操作可能是内部实现细节,因此即使看似“只读”的操作(如`get()`)实际上也可能执行一些写操作,例如更新某些统计信息(或在访问顺序的`LinkedHashMap`的情况下,更新访问顺序)。因此,良好编写的类应该提供一些文档,明确说明如果...

...明显的“只读”操作在线程安全意义上是否真的是内部只读的。例如,C++标准库中有一个总则,标记为`const`的成员函数在这种意义上真正是只读的(在内部,它们可能仍然执行写操作,但这些操作必须是线程安全的)。在Java中没有`const`关键字,我也不知道有任何书面记录的普遍保证,但是通常标准库类的行为都是符合预期的,并且有文档记录了异常情况(例如,`LinkedHashMap`的示例中明确提到了像`get`这样的只读操作是不安全的)。

- 最后,回到您最初的问题,对于`HashMap`,我们实际上在[文档](https://docs.oracle.com/javase/8/docs/api/java/util/HashMap.html)中确实有线程安全性的说明:如果多个线程同时访问哈希映射,并且至少有一个线程对映射进行结构上的修改,则必须在外部进行同步。(结构修改是指添加或删除一个或多个映射的任何操作;仅更改实例已经包含的键关联的值不是结构修改。)

因此,对于我们希望是只读的`HashMap`方法,它们在不进行结构修改的情况下是只读的。当然,这个保证可能不适用于任意其他`Map`实现,但是该问题特指`HashMap`。

0
0 Comments

从上述内容中可以得出以下问题的出现原因和解决方法:

问题:在多线程环境下,从java.util.HashMap中获取值是否安全(没有修改)?

原因:问题实质上是在问“访问不可变HashMap是否安全”,答案是肯定的。但是,必须回答这个问题的前提条件,即“我的HashMap是否是不可变的”。Java有一套相对复杂的规则来确定不可变性。

解决方法:阅读Jeremy Manson关于此主题的博客文章,他是Java内存模型方面的专家。以下是他的博客文章链接:

Immutability in Java的第一部分:

http://jeremymanson.blogspot.com/2008/04/immutability-in-java.html

Immutability in Java的第二部分:

http://jeremymanson.blogspot.com/2008/07/immutability-in-java-part-2.html

Immutability in Java的第三部分:

http://jeremymanson.blogspot.com/2008/07/immutability-in-java-part-3.html

以上是问题的出现原因和解决方法。

0