考虑这样的情况。
有两个线程和一个共享资源(如HashMap)。一个线程创建了HashMap并使用一些键值对初始化它,并且在初始化共享资源之后,它将永远不会再次被修改。
现在,第二个线程是在初始化共享资源并且想要使用该资源之后严格创建的。此时我想保证第二个线程将使用正确版本的共享资源。我假设第一个线程可能在创建第二个线程之前没有刷新对主内存的更改,因此第二个线程将共享资源的旧值带到它的缓存中。
此分析是否正确,以及如何在初始化共享资源后手动强制刷新到Java中的主内存,就像我不想要或不需要volatile
或synchronized
的特殊情况一样。 / p>
答案 0 :(得分:4)
答案 1 :(得分:1)
如果您将HashMap声明并初始化为静态字段,它将以线程安全的方式由Java类加载器初始化。
答案 2 :(得分:1)
如果地图初始化发生在第二个线程的开始之前,那么一切都是正确的。为了简化分析并简化操作,您可以将初始化的映射转换为一些不可变的映射实现,并将其显式传递给创建的线程。这样你根本不需要使用共享变量。
答案 3 :(得分:1)
此分析是否正确,以及如何在初始化共享资源后手动强制刷新到Java中的主内存,就像我不想要或不需要volatile或同步的特殊情况一样。
不可能不需要volatile
或synchronized
。你必须在线程之间使用某种形式的内存同步,否则东西不起作用。
您可以使用static
初始化程序,如Andrei提到的(*)或final
,这两者都意味着内存障碍。但你必须使用一些东西。
您可能需要同步地图(Collections.synchronizedMap())或CurrentHashMap,但仍需要使用volatile
,synchronized
,final
或static
保护战场本身。
C.f。 Brian Geotz Java Concurrency in Practice以及此related question on Stack Overflow(请注意,OP会将本书的名称弄错)。
(*整个静态初始化器有点复杂,你应该阅读Goetz先生的书,但我会试着简要描述一下:static
字段是类初始化的一部分。每个静态字段或静态初始化程序块由一个线程编写或执行(可能是调用new
的线程或第一次访问类对象,或者可能是一个不同的线程)。当编写所有{{{首次完成1}}字段,JVM插入一个内存屏障,以便类对象及其所有静态字段对于系统中的所有线程都是可见的。
每个字段写入都没有内存屏障,例如static
。类加载尝试高效,并且只在初始化的最后插入一个屏障。因此,您应该只使用volatile
初始化器来实现它们的用途:第一次填充字段,而不是尝试在静态初始化程序块中编写整个程序。它效率不高,您对线程安全的选择实际上更有限。
然而,可以使用类初始化的内存屏障,这就是为什么,Andrei Amarfii说,在Java中使用静态初始化器的模式用于确保对象的可见性。重要的是Brian Goetz将其称为他的四种“安全出版”模式之一。)