我想在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...
}
而且我得到了“Double-Checked Locking is Broken”声明。 我怎么解决这个问题?
这是项目71中推荐的习语:明智地使用有效Java的懒惰初始化:
如果需要在实例字段上使用延迟初始化来提高性能,请使用双重检查惯用法。这个习惯用法避免了在初始化之后访问字段时的锁定成本(第67项)。成语背后的想法是检查字段的值两次(因此名称仔细检查):一次没有锁定,然后,如果字段看起来未初始化,则第二次锁定。仅当第二次检查表明该字段未初始化时,该呼叫才会初始化该字段。因为如果字段已经初始化,则没有锁定,因此将字段声明为
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]。今天,双重检查成语是懒惰初始化实例字段的首选技术。虽然您也可以将双重检查成语应用于静态字段,但没有理由这样做:延迟初始化持有者类习惯用法是更好的选择。
这是一个正确的双重检查锁定的模式。
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;
}
}
在Java中正确执行双重检查锁定的唯一方法是对有问题的变量使用“volatile”声明。虽然该解决方案是正确的,但请注意“volatile”意味着在每次访问时都会刷新缓存行。由于“同步”在块的末尾刷新它们,它实际上可能不再有效(或甚至效率更低)。我建议不要使用双重检查锁定,除非您已经分析了代码并发现此区域存在性能问题。
DCL使用ThreadLocal由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的DCL
class 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一样 - 有明确的机器指令用于执行内存屏障。)当一个线程退出同步块时,它会执行写屏障 - 它必须在释放之前将该块中修改的任何变量清除到主内存中锁。类似地,当进入同步块时,它执行读屏障 - 就好像本地存储器已经无效,并且它必须从主存储器中获取将在块中引用的任何变量。
你是什么意思,从谁那里得到宣言?
双重检查锁定是固定的。检查维基百科:
class Foo {
private volatile Helper helper = null;
public Helper getHelper() {
if (helper == null) {
synchronized(this) {
if (helper == null)
helper = new Helper();
}
}
return helper;
}
}
正如一些人已经指出的那样,你肯定需要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;
}
关键字才能使它正常工作,除非对象中的所有成员都被声明为volatile
,否则在安全发布之前没有发生,你可以看到默认值。
我们厌倦了人们犯这个错误的常见问题,所以我们编写了一个final
实用程序,它具有最终的语义,并且已经被描述和调整为尽可能快。
从下面的其他地方复制,这解释了为什么使用方法局部变量作为volatile变量的副本会加快速度。
需要说明的声明:
此代码可能看起来有点复杂。特别是,对局部变量结果的需求可能不清楚。
说明:
该字段将在第一个if语句中第一次读取,第二次在return语句中读取。该字段被声明为volatile,这意味着每次访问时都必须从内存中重新获取(粗略地说,访问volatile变量可能需要更多处理),并且不能由编译器存储到寄存器中。当复制到局部变量然后在两个语句中使用(if和return)时,寄存器优化可以由JVM完成。
如果我没有弄错的话,如果我们不想使用volatile关键字,还有另一种解决方案
例如,通过前面的例子
LazyReference
测试总是在辅助变量上,但是对象的构造就在newHelper之前完成,它避免了部分构造的对象