我想在Java中实现多线程的延迟初始化 我有一些类似的代码:
class Foo {
private Helper helper = null;
public Helper getHelper() {
if (helper == null) {
Helper h;
synchronized(this) {
h = helper;
if (h == null)
synchronized (this) {
h = new Helper();
} // release inner synchronization lock
helper = h;
}
}
return helper;
}
// other functions and members...
}
我正在接受“双重检查已破损”声明 我该如何解决这个问题?
答案 0 :(得分:70)
以下是项目71:明智地使用懒惰初始化中推荐的习语 有效的Java:
如果你需要使用延迟初始化来提高性能 实例字段,使用仔细检查 成语 即可。这个成语避免了成本 在访问该字段时锁定 初始化之后(项目 67)。成语背后的想法是 检查字段的值两次 (因此名称仔细检查):一次 没有锁定,然后,如果 字段似乎未初始化,a 第二次锁定。只有当 第二次检查表明该字段 是未初始化的电话 初始化该字段。因为有 如果该字段已经没有锁定 初始化,它是 critical 字段声明
volatile
(项目 66)。这是成语:// Double-check idiom for lazy initialization of instance fields private volatile FieldType field; private FieldType getField() { FieldType result = field; if (result != null) // First check (no locking) return result; synchronized(this) { if (field == null) // Second check (with locking) field = computeFieldValue(); return field; } }
此代码可能看起来有点复杂。 特别需要当地人 变量结果可能不清楚。什么 这个变量确实是为了确保这一点 字段在公共中只读一次 它已经初始化的情况。 虽然不是绝对必要,但这可能 提高性能,更多 优雅的标准适用于 低级并发编程。上 我的机器,上面的方法是关于 比显而易见的快25% 没有局部变量的版本。
在1.5版之前,仔细检查 成语不能可靠地工作,因为 volatile修饰符的语义 不足以支持它 [Pugh01]。内存模型介绍 在1.5版中解决了这个问题 [JLS,17,Goetz06 16]。今天, 复核成语是一种技巧 懒惰地初始化的选择 实例字段。虽然你可以申请 对于静态的复核成语 领域也是如此,没有理由 这样做:懒惰的初始化持有者 成语是一个更好的选择。
答案 1 :(得分:12)
这是正确的双重检查锁定的模式。
class Foo {
private volatile HeavyWeight lazy;
HeavyWeight getLazy() {
HeavyWeight tmp = lazy; /* Minimize slow accesses to `volatile` member. */
if (tmp == null) {
synchronized (this) {
tmp = lazy;
if (tmp == null)
lazy = tmp = createHeavyWeightObject();
}
}
return tmp;
}
}
对于单身人士来说,延迟初始化有一个更易读的习惯用法。
class Singleton {
private static class Ref {
static final Singleton instance = new Singleton();
}
public static Singleton get() {
return Ref.instance;
}
}
答案 2 :(得分:3)
在Java中正确执行双重检查锁定的唯一方法是对相关变量使用“volatile”声明。虽然该解决方案是正确的,但请注意“volatile”意味着在每次访问时都会刷新缓存行。由于“同步”在块的末尾刷新它们,它实际上可能不再有效(或甚至效率更低)。我建议不要使用双重检查锁定,除非你已经分析了你的代码,发现这个领域存在性能问题。
答案 3 :(得分:3)
DCL使用ThreadLocal By Brian Goetz @ JavaWorld
关于DCL的内容是什么?
DCL依赖于资源字段的不同步使用。这似乎是无害的,但事实并非如此。为了了解原因,假设线程A在synchronized块内,执行语句resource = new Resource();而线程B只是进入getResource()。考虑这种初始化对内存的影响。将分配新Resource对象的内存;将调用Resource的构造函数,初始化新对象的成员字段;并且将为SomeClass的字段资源分配对新创建的对象的引用。
class SomeClass {
private Resource resource = null;
public Resource getResource() {
if (resource == null) {
synchronized {
if (resource == null)
resource = new Resource();
}
}
return resource;
}
}
然而,由于线程B没有在同步块内执行,因此它可能以与一个线程A执行的顺序不同的顺序看到这些存储器操作。可能是B按以下顺序看到这些事件的情况(并且编译器也可以自由地重新排序这样的指令):分配内存,分配对资源的引用,调用构造函数。假设线程B在分配了内存并且设置了资源字段之后但在调用构造函数之前出现。它看到资源不为null,跳过synchronized块,并返回对部分构造的Resource的引用!毋庸置疑,结果既不是预期也不是期望。
ThreadLocal可以帮助修复DCL吗?
我们可以使用ThreadLocal来实现DCL惯用语的明确目标 - 在公共代码路径上没有同步的延迟初始化。考虑DCL的这个(线程安全)版本:
清单2.使用ThreadLocal的
的DCLclass ThreadLocalDCL {
private static ThreadLocal initHolder = new ThreadLocal();
private static Resource resource = null;
public Resource getResource() {
if (initHolder.get() == null) {
synchronized {
if (resource == null)
resource = new Resource();
initHolder.set(Boolean.TRUE);
}
}
return resource;
}
}
我想;这里每个线程都会进入SYNC块以更新threadLocal值;然后它不会。因此,ThreadLocal DCL将确保线程仅在SYNC块内输入一次。
同步到底意味着什么?
Java将每个线程视为在其自己的处理器上运行,并具有自己的本地内存,每个线程与共享主内存进行通信并同步。即使在单处理器系统上,由于内存缓存的影响以及使用处理器寄存器来存储变量,该模型也是有意义的。当线程修改其本地内存中的位置时,该修改最终也应该显示在主内存中,并且JMM定义JVM何时必须在本地内存和主内存之间传输数据的规则。 Java架构师意识到过度限制的内存模型会严重破坏程序性能。他们试图制作一种内存模型,使程序在现代计算机硬件上运行良好,同时仍然提供允许线程以可预测的方式进行交互的保证。
Java用于在线程之间呈现交互的主要工具可预测地是synchronized关键字。许多程序员认为在强制执行互斥信号量(mutex)方面严格同步,以防止一次多个线程执行关键部分。不幸的是,这种直觉并没有完全描述同步意味着什么。
synchronized的语义确实包括基于信号量的状态互斥执行,但它们还包括有关同步线程与主存储器的交互的规则。特别是,获取或释放锁会触发内存屏障 - 线程本地内存和主内存之间的强制同步。 (某些处理器 - 像Alpha一样 - 有明确的机器指令用于执行内存屏障。)当一个线程退出同步块时,它会执行写屏障 - 它必须在释放之前将该块中修改的任何变量清除到主内存中锁。类似地,当进入同步块时,它执行读屏障 - 就好像本地存储器已经无效,并且它必须从主存储器中获取将在块中引用的任何变量。
答案 4 :(得分:2)
定义应使用volatile
midifier
您不需要h
变量。
以下是here
class Foo {
private volatile Helper helper = null;
public Helper getHelper() {
if (helper == null) {
synchronized(this) {
if (helper == null)
helper = new Helper();
}
}
return helper;
}
}
答案 5 :(得分:2)
双重检查锁定是固定的。检查维基百科:
public class FinalWrapper<T>
{
public final T value;
public FinalWrapper(T value) { this.value = value; }
}
public class Foo
{
private FinalWrapper<Helper> helperWrapper = null;
public Helper getHelper()
{
FinalWrapper<Helper> wrapper = helperWrapper;
if (wrapper == null)
{
synchronized(this)
{
if (helperWrapper ==null)
helperWrapper = new FinalWrapper<Helper>( new Helper() );
wrapper = helperWrapper;
}
}
return wrapper.value;
}
答案 6 :(得分:2)
正如一些人所指出的那样,你肯定需要volatile
关键字来使其正常工作,除非对象中的所有成员都被声明为final
,否则没有发生 - 在pr safe-publication之前你可以看到默认值。
我们厌倦了人们犯这个错误的常见问题,所以我们编写了一个LazyReference实用程序,它具有最终语义,并且已被分析和调整为尽可能快。
答案 7 :(得分:2)
从下面的其他地方复制,这解释了为什么使用方法局部变量作为volatile变量的副本会加快速度。
需要解释的声明:
此代码可能看起来有点复杂。特别需要的 局部变量结果可能不清楚。
说明:
该字段将在第一个if语句中首次读取 第二次在退货声明中。该字段声明为volatile, 这意味着它必须每次都从内存中重新获取 访问(粗略地说,可能需要更多的处理 访问volatile变量)并且不能存储到寄存器中 编译器。复制到局部变量然后在两者中使用 语句(if和return),寄存器优化可以通过 JVM。
答案 8 :(得分:-2)
如果我没有弄错的话,如果我们不想使用volatile关键字,还有另一种解决方案
例如通过前面的例子
class Foo {
private Helper helper = null;
public Helper getHelper() {
if (helper == null) {
synchronized(this) {
if (helper == null)
Helper newHelper = new Helper();
helper = newHelper;
}
}
return helper;
}
}
测试总是在辅助变量上,但是对象的构造就在newHelper之前完成,它避免了部分构造的对象