mon*_*lbo 33 java concurrency multithreading locking lazy-loading
我想在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...
}
Run Code Online (Sandbox Code Playgroud)
而且我得到了"Double-Checked Locking is Broken"声明.
我怎么解决这个问题?
Pas*_*ent 73
这是项目71中推荐的习语:明智地使用有效Java的懒惰初始化:
如果需要在实例字段上使用延迟初始化来提高性能,请使用双重检查惯用法.这个习惯用法避免了在初始化之后访问字段时的锁定成本(第67项).习语背后的想法是检查字段的值两次(因此名称仔细检查):一次没有锁定,然后,如果字段看起来未初始化,则第二次锁定.仅当第二次检查表明该字段未初始化时,该呼叫才会初始化该字段.因为如果字段已经初始化,则没有锁定,因此声明字段是至关重要的
volatile(条目66).这是成语:Run Code Online (Sandbox Code Playgroud)// 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].今天,双重检查成语是懒惰初始化实例字段的首选技术.虽然您也可以将双重检查成语应用于静态字段,但没有理由这样做:延迟初始化持有者类习惯用法是更好的选择.
eri*_*son 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;
}
}
Run Code Online (Sandbox Code Playgroud)
对于单例,有一个更易读的习惯用于延迟初始化.
class Singleton {
private static class Ref {
static final Singleton instance = new Singleton();
}
public static Singleton get() {
return Ref.instance;
}
}
Run Code Online (Sandbox Code Playgroud)
DCL 使用 ThreadLocal 作者:Brian Goetz @ JavaWorld
DCL 有什么问题?
DCL 依赖于资源字段的非同步使用。这似乎是无害的,但事实并非如此。要了解原因,假设线程 A 在同步块内,执行语句 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;
}
}
Run Code Online (Sandbox Code Playgroud)
但是,由于线程 B 不在同步块内执行,因此它可能会以与线程 A 执行的顺序不同的顺序查看这些内存操作。B 可能会按照以下顺序看到这些事件(并且编译器也可以像这样自由地重新排序指令):分配内存,分配对资源的引用,调用构造函数。假设线程 B 在分配内存并设置资源字段之后,但在调用构造函数之前出现。它看到资源不为空,跳过同步块,并返回对部分构造的资源的引用!不用说,结果既不是预期的,也不是想要的。
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;
}
}
Run Code Online (Sandbox Code Playgroud)
我认为; 这里每个线程都会一次进入 SYNC 块来更新 threadLocal 值;那么它不会。所以 ThreadLocal DCL 将确保一个线程在 SYNC 块中只进入一次。
同步的真正含义是什么?
Java 将每个线程视为在自己的处理器上运行,并拥有自己的本地内存,每个线程都与共享主内存通信并与之同步。即使在单处理器系统上,由于内存缓存的影响和使用处理器寄存器来存储变量,该模型也有意义。当线程修改其本地内存中的某个位置时,该修改最终也应显示在主内存中,并且 JMM 定义了 JVM 何时必须在本地和主内存之间传输数据的规则。Java 架构师意识到过度限制的内存模型会严重破坏程序性能。他们试图构建一个内存模型,让程序在现代计算机硬件上运行良好,同时仍然提供保证,允许线程以可预测的方式交互。
Java 用于可预测地呈现线程间交互的主要工具是 synchronized 关键字。许多程序员严格按照强制互斥信号量 (mutex) 来防止同时由多个线程执行临界区来考虑同步。不幸的是,这种直觉并没有完全描述同步的含义。
同步的语义确实包括基于信号量状态的互斥执行,但它们也包括有关同步线程与主内存交互的规则。特别是,锁定的获取或释放会触发内存屏障——线程的本地内存和主内存之间的强制同步。(一些处理器——比如 Alpha——有明确的机器指令来执行内存屏障。)当一个线程退出一个同步块时,它会执行一个写屏障——它必须在释放之前将该块中修改的任何变量刷新到主内存中锁。类似地,当进入一个同步块时,它会执行一个读屏障——就好像本地内存已经失效一样,它必须从主内存中获取将在块中引用的任何变量。
| 归档时间: |
|
| 查看次数: |
20175 次 |
| 最近记录: |