BT

双重检查锁定与延迟初始化

作者 程晓明 发布于 2013年10月7日 | 被首富的“一个亿”刷屏?不如定个小目标,先把握住QCon上海的优惠吧!

在java程序中,有时候可能需要推迟一些高开销的对象初始化操作,并且只有在使用这些对象时才进行初始化。此时程序员可能会采用延迟初始化。但要正确实现线程安全的延迟初始化需要一些技巧,否则很容易出现问题。比如,下面是非线程安全的延迟初始化对象的示例代码:

public class UnsafeLazyInitialization {
private static Instance instance;
public static Instance getInstance() {
if (instance == null) //1:A线程执行
instance = new Instance(); //2:B线程执行
return instance;
}
}

在UnsafeLazyInitialization中,假设A线程执行代码1的同时,B线程执行代码2。此时,线程A可能会看到instance引用的对象还没有完成初始化(出现这种情况的原因见后文的“问题的根源”)。

对于UnsafeLazyInitialization,我们可以对getInstance()做同步处理来实现线程安全的延迟初始化。示例代码如下:

public class SafeLazyInitialization {
    private static Instance instance;

    public synchronized static Instance getInstance() {
        if (instance == null)
            instance = new Instance();
        return instance;
    }
}

由于对getInstance()做了同步处理,synchronized将导致性能开销。如果getInstance()被多个线程频繁的调用,将会导致程序执行性能的下降。反之,如果getInstance()不会被多个线程频繁的调用,那么这个延迟初始化方案将能提供令人满意的性能。

在早期的JVM中,synchronized(甚至是无竞争的synchronized)存在这巨大的性能开销。因此,人们想出了一个“聪明”的技巧:双重检查锁定(double-checked locking)。人们想通过双重检查锁定来降低同步的开销。下面是使用双重检查锁定来实现延迟初始化的示例代码:

public class DoubleCheckedLocking {                 //1
    private static Instance instance;                    //2

    public static Instance getInstance() {               //3
        if (instance == null) {                          //4:第一次检查
            synchronized (DoubleCheckedLocking.class) {  //5:加锁
                if (instance == null)                    //6:第二次检查
                    instance = new Instance();           //7:问题的根源出在这里
            }                                            //8
        }                                                //9
        return instance;                                 //10
    }                                                    //11
}                                                        //12

如上面代码所示,如果第一次检查instance不为null,那么就不需要执行下面的加锁和初始化操作。因此可以大幅降低synchronized带来的性能开销。上面代码表面上看起来,似乎两全其美:

  • 在多个线程试图在同一时间创建对象时,会通过加锁来保证只有一个线程能创建对象。
  • 在对象创建好之后,执行getInstance()将不需要获取锁,直接返回已创建好的对象。

双重检查锁定看起来似乎很完美,但这是一个错误的优化!在线程执行到第4行代码读取到instance不为null时,instance引用的对象有可能还没有完成初始化。

问题的根源

前面的双重检查锁定示例代码的第7行(instance = new Singleton();)创建一个对象。这一行代码可以分解为如下的三行伪代码:

memory = allocate();   //1:分配对象的内存空间
ctorInstance(memory);  //2:初始化对象
instance = memory;     //3:设置instance指向刚分配的内存地址

上面三行伪代码中的2和3之间,可能会被重排序(在一些JIT编译器上,这种重排序是真实发生的,详情见参考文献1的“Out-of-order writes”部分)。2和3之间重排序之后的执行时序如下:

memory = allocate();   //1:分配对象的内存空间
instance = memory;     //3:设置instance指向刚分配的内存地址
                       //注意,此时对象还没有被初始化!
ctorInstance(memory);  //2:初始化对象

根据《The Java Language Specification, Java SE 7 Edition》(后文简称为java语言规范),所有线程在执行java程序时必须要遵守intra-thread semantics。intra-thread semantics保证重排序不会改变单线程内的程序执行结果。换句话来说,intra-thread semantics允许那些在单线程内,不会改变单线程程序执行结果的重排序。上面三行伪代码的2和3之间虽然被重排序了,但这个重排序并不会违反intra-thread semantics。这个重排序在没有改变单线程程序的执行结果的前提下,可以提高程序的执行性能。

为了更好的理解intra-thread semantics,请看下面的示意图(假设一个线程A在构造对象后,立即访问这个对象):

如上图所示,只要保证2排在4的前面,即使2和3之间重排序了,也不会违反intra-thread semantics。

下面,再让我们看看多线程并发执行的时候的情况。请看下面的示意图:

由于单线程内要遵守intra-thread semantics,从而能保证A线程的程序执行结果不会被改变。但是当线程A和B按上图的时序执行时,B线程将看到一个还没有被初始化的对象。

※注:本文统一用红色的虚箭线标识错误的读操作,用绿色的虚箭线标识正确的读操作。

回到本文的主题,DoubleCheckedLocking示例代码的第7行(instance = new Singleton();)如果发生重排序,另一个并发执行的线程B就有可能在第4行判断instance不为null。线程B接下来将访问instance所引用的对象,但此时这个对象可能还没有被A线程初始化!下面是这个场景的具体执行时序:

时间

线程A

线程B

t1

A1:分配对象的内存空间

 

t2

A3:设置instance指向内存空间

 

t3

 

B1:判断instance是否为空

t4

 

B2:由于instance不为null,线程B将访问instance引用的对象

t5

A2:初始化对象

 

t6

A4:访问instance引用的对象

 

这里A2和A3虽然重排序了,但java内存模型的intra-thread semantics将确保A2一定会排在A4前面执行。因此线程A的intra-thread semantics没有改变。但A2和A3的重排序,将导致线程B在B1处判断出instance不为空,线程B接下来将访问instance引用的对象。此时,线程B将会访问到一个还未初始化的对象。

在知晓了问题发生的根源之后,我们可以想出两个办法来实现线程安全的延迟初始化:

  1. 不允许2和3重排序;
  2. 允许2和3重排序,但不允许其他线程“看到”这个重排序。

后文介绍的两个解决方案,分别对应于上面这两点。

基于volatile的双重检查锁定的解决方案

对于前面的基于双重检查锁定来实现延迟初始化的方案(指DoubleCheckedLocking示例代码),我们只需要做一点小的修改(把instance声明为volatile型),就可以实现线程安全的延迟初始化。请看下面的示例代码:

public class SafeDoubleCheckedLocking {
    private volatile static Instance instance;

    public static Instance getInstance() {
        if (instance == null) {
            synchronized (SafeDoubleCheckedLocking.class) {
                if (instance == null)
                    instance = new Instance();//instance为volatile,现在没问题了
            }
        }
        return instance;
    }
}

注意,这个解决方案需要JDK5或更高版本(因为从JDK5开始使用新的JSR-133内存模型规范,这个规范增强了volatile的语义)。

当声明对象的引用为volatile后,“问题的根源”的三行伪代码中的2和3之间的重排序,在多线程环境中将会被禁止。上面示例代码将按如下的时序执行:

这个方案本质上是通过禁止上图中的2和3之间的重排序,来保证线程安全的延迟初始化。

基于类初始化的解决方案

JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。

基于这个特性,可以实现另一种线程安全的延迟初始化方案(这个方案被称之为Initialization On Demand Holder idiom):

public class InstanceFactory {
    private static class InstanceHolder {
        public static Instance instance = new Instance();
    }

    public static Instance getInstance() {
        return InstanceHolder.instance ;  //这里将导致InstanceHolder类被初始化
    }
}

假设两个线程并发执行getInstance(),下面是执行的示意图:

这个方案的实质是:允许“问题的根源”的三行伪代码中的2和3重排序,但不允许非构造线程(这里指线程B)“看到”这个重排序。

初始化一个类,包括执行这个类的静态初始化和初始化在这个类中声明的静态字段。根据java语言规范,在首次发生下列任意一种情况时,一个类或接口类型T将被立即初始化:

  • T是一个类,而且一个T类型的实例被创建;
  • T是一个类,且T中声明的一个静态方法被调用;
  • T中声明的一个静态字段被赋值;
  • T中声明的一个静态字段被使用,而且这个字段不是一个常量字段;
  • T是一个顶级类(top level class,见java语言规范的§7.6),而且一个断言语句嵌套在T内部被执行。

在InstanceFactory示例代码中,首次执行getInstance()的线程将导致InstanceHolder类被初始化(符合情况4)。

由于java语言是多线程的,多个线程可能在同一时间尝试去初始化同一个类或接口(比如这里多个线程可能在同一时刻调用getInstance()来初始化InstanceHolder类)。因此在java中初始化一个类或者接口时,需要做细致的同步处理。

Java语言规范规定,对于每一个类或接口C,都有一个唯一的初始化锁LC与之对应。从C到LC的映射,由JVM的具体实现去自由实现。JVM在类初始化期间会获取这个初始化锁,并且每个线程至少获取一次锁来确保这个类已经被初始化过了(事实上,java语言规范允许JVM的具体实现在这里做一些优化,见后文的说明)。

对于类或接口的初始化,java语言规范制定了精巧而复杂的类初始化处理过程。java初始化一个类或接口的处理过程如下(这里对类初始化处理过程的说明,省略了与本文无关的部分;同时为了更好的说明类初始化过程中的同步处理机制,笔者人为的把类初始化的处理过程分为了五个阶段):

第一阶段:通过在Class对象上同步(即获取Class对象的初始化锁),来控制类或接口的初始化。这个获取锁的线程会一直等待,直到当前线程能够获取到这个初始化锁。

假设Class对象当前还没有被初始化(初始化状态state此时被标记为state = noInitialization),且有两个线程A和B试图同时初始化这个Class对象。下面是对应的示意图:

下面是这个示意图的说明:

时间

线程A

线程B

t1

A1:尝试获取Class对象的初始化锁。这里假设线程A获取到了初始化锁

B1:尝试获取Class对象的初始化锁,由于线程A获取到了锁,线程B将一直等待获取初始化锁

t2

A2:线程A看到线程还未被初始化(因为读取到state == noInitialization),线程设置state = initializing

 

t3

A3:线程A释放初始化锁

 

第二阶段:线程A执行类的初始化,同时线程B在初始化锁对应的condition上等待:

下面是这个示意图的说明:

时间

线程A

线程B

t1

A1:执行类的静态初始化和初始化类中声明的静态字段

B1:获取到初始化锁

t2

 

B2:读取到state == initializing

t3

 

B3:释放初始化锁

t4

 

B4:在初始化锁的condition中等待

第三阶段:线程A设置state = initialized,然后唤醒在condition中等待的所有线程:

下面是这个示意图的说明:

时间

线程A

t1

A1:获取初始化锁

t2

A2:设置state = initialized

t3

A3:唤醒在condition中等待的所有线程

t4

A4:释放初始化锁

t5

A5:线程A的初始化处理过程完成

第四阶段:线程B结束类的初始化处理:

下面是这个示意图的说明:

时间

线程B

t1

B1:获取初始化锁

t2

B2:读取到state == initialized

t3

B3:释放初始化锁

t4

B4:线程B的类初始化处理过程完成

线程A在第二阶段的A1执行类的初始化,并在第三阶段的A4释放初始化锁;线程B在第四阶段的B1获取同一个初始化锁,并在第四阶段的B4之后才开始访问这个类。根据java内存模型规范的锁规则,这里将存在如下的happens-before关系:

这个happens-before关系将保证:线程A执行类的初始化时的写入操作(执行类的静态初始化和初始化类中声明的静态字段),线程B一定能看到。

第五阶段:线程C执行类的初始化的处理:

下面是这个示意图的说明:

时间

线程B

t1

C1:获取初始化锁

t2

C2:读取到state == initialized

t3

C3:释放初始化锁

t4

C4:线程C的类初始化处理过程完成

在第三阶段之后,类已经完成了初始化。因此线程C在第五阶段的类初始化处理过程相对简单一些(前面的线程A和B的类初始化处理过程都经历了两次锁获取-锁释放,而线程C的类初始化处理只需要经历一次锁获取-锁释放)。

线程A在第二阶段的A1执行类的初始化,并在第三阶段的A4释放锁;线程C在第五阶段的C1获取同一个锁,并在在第五阶段的C4之后才开始访问这个类。根据java内存模型规范的锁规则,这里将存在如下的happens-before关系:

这个happens-before关系将保证:线程A执行类的初始化时的写入操作,线程C一定能看到。

※注1:这里的condition和state标记是本文虚构出来的。Java语言规范并没有硬性规定一定要使用condition和state标记。JVM的具体实现只要实现类似功能即可。

※注2:Java语言规范允许Java的具体实现,优化类的初始化处理过程(对这里的第五阶段做优化),具体细节参见java语言规范的12.4.2章。

通过对比基于volatile的双重检查锁定的方案和基于类初始化的方案,我们会发现基于类初始化的方案的实现代码更简洁。但基于volatile的双重检查锁定的方案有一个额外的优势:除了可以对静态字段实现延迟初始化外,还可以对实例字段实现延迟初始化。

总结

延迟初始化降低了初始化类或创建实例的开销,但增加了访问被延迟初始化的字段的开销。在大多数时候,正常的初始化要优于延迟初始化。如果确实需要对实例字段使用线程安全的延迟初始化,请使用上面介绍的基于volatile的延迟初始化的方案;如果确实需要对静态字段使用线程安全的延迟初始化,请使用上面介绍的基于类初始化的方案。

参考文献

  1. Double-checked locking and the Singleton pattern
  2. The Java Language Specification, Java SE 7 Edition
  3. JSR-133: Java Memory Model and Thread Specification
  4. Java Concurrency in Practice
  5. Effective Java (2nd Edition)
  6. JSR 133 (Java Memory Model) FAQ
  7. The JSR-133 Cookbook for Compiler Writers
  8. Java theory and practice: Fixing the Java Memory Model, Part 2

个人简介

程晓明,java软件工程师,专注于并发编程,现就职于富士通南大。个人邮箱:asst2003@163.com。


感谢方腾飞对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ)或者腾讯微博(@InfoQ)关注我们,并与我们的编辑和其他读者朋友交流。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我
社区评论

还有个方法 by 杨 浩

instance = new Instance();
如果变成呢?
Instance tmp = new Instance();
instance = tmp;

好文! by Tse Zac

之前只知道使用volatile和static holder这两种方式,却没有进一步深挖以探寻他们的根本区别在于对重排序这种优化手段应对策略的不同。另外关于static字段初始化的细致处理也是第一次在这里看到。
楼主的文章让小弟的理解又上了一层,拜谢!
另外再加一点我个人的理解,对于DCL根本的问题是在于data race,所有这一切都是在处理数据竞争这个问题,因为多线程环境下如果有数据竞争行为就不可预测了。

Re: 还有个方法 by 程 晓明

当你改写成下面两行代码时,JVM可能会把这两行代码优化成上面的那一行代码。
问题依然会存在。

Re: 好文! by 程 晓明

很高兴这篇文章能对您有所帮助,谢谢您的关注!

Re: 还有个方法 by 杨 浩

“可能?”
实际上从jvm字节码来看,并没有被优化掉tmp。
只要声明了变量,就肯定会占用一个栈位置,不会优化掉的。

貌似基本都是IBM文章的翻译 by Tim rex

可以多看看参考中的IBM文章。

Re: 还有个方法 by 程 晓明

谢谢您的回复。

常见的JVM(Oracle的HotSpot和IBM的J9)都是采用解释器与编译器并存的架构。

下面是Oracle官方对HotSpot虚拟机的说明,其中第三章讲到了JIT编译器:
www.oracle.com/technetwork/java/whitepaper-1352...
如第三章所言,JIT可以把字节码翻译成为本地平台相关的机器码,并做各种优化。
而且,对于Java HotSpot Server Compiler(即C2)来说,它会执行所有的经典的优化(The optimizer performs all the classic optimizations)。

对于下面的那两行代码,JIT可以把它们优化成上面的那一行代码。

Re: 貌似基本都是IBM文章的翻译 by 程 晓明

谢谢您的关注。

双重检查锁定问题,以及正确实现延迟初始化的技巧,在参考文献1,参考文献4的16.2,以及参考文献5的第71条都有相应的说明。

本文并不是要重述三位专家的论述,三位专家对为什么会出错以及怎么样写才是正确的,已有定论。
我写这篇文章是为了分析双重检查锁定之所以会出错的深层原因,以及为什么那些正确的延迟初始化写法能保证正确性。

这三个参考文献说明了what,我试图在这篇文章中说明why:

参考文献1中指出,“instance = new Instance();”可能会被重排序,但并没有说明java为什么允许这个重排序?本文对此做了分析。

参考文献4和5都指出:双重检查锁定添加volatile就可以保证正确性。但没有说明为什么增加volatile声明后,双重检查锁定就变成线程安全的了?有些读者给我写信问这个问题,这也是我最初决定写这篇文章的原因。

参考文献4和5都指出:利用类初始化的同步处理机制,可以实现线程安全的延迟初始化。但我看到的所有的文献,都没有讲过类初始化的时候到底做了什么样的同步处理?我在这篇文章中对类初始化的同步处理的分析,完全是基于java语言规范,没有参考任何其它的资料。

由于需要向读者说明相关背景知识,所以文章的开头部分与参考文献1类似,同时中间和后面部分有几句话也是从参考文献4和5中“借鉴”过来的:)

双重检查的疑问 by fazhong tan

最近几天正在研究这个问题,写了一个测试,发现在JDK6上加了volatile也不行!
package com.thread.self.singleInstance;

public class SingleInstance4 {
private static volatile String[] values;//不管是否加volatile关键字都不会出现不安全发布现象
public static String[] getInstance() throws InterruptedException{
if(null==values){
synchronized(SingleInstance4.class){
if(null==values){
values=new String[1000000];//不安全发布
int num=1000000;
for(int i=999999;i>0;i--){
values[num--]=new Integer(i).toString()+Thread.currentThread().getName();
}
}
}
}
return values;
}
}

package com.thread.self.singleInstance;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class AllTester {
static Runnable createRunnable(){
Runnable run=new Runnable(){
@Override
public void run() {
String[] va;
try {
//va = SingleInstance.getInstance();
//va = SingInstance2.getInstance();
//va = SingInstance3.getInstance();
//va = VolatileSingle.getInstance();
va=SingleInstance4.getInstance();
//va = staticSingleInstance.getInstance();
print(va);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

};
return run;
}

/**
* @param args
*/
public static void main(String[] args) {
ExecutorService es=Executors.newCachedThreadPool();
Runnable r1=createRunnable();
Runnable r2=createRunnable();
Runnable r3=createRunnable();
es.submit(r1);
es.submit(r2);
es.submit(r3);
es.shutdown();
}
static void print(String[] v){
boolean isNull=false;
for(int i=0;i<v.length;i++){
if(null==v){
isNull=true;
break;
//System.out.println("At thread name is "+Thread.currentThread().getName()+" of bit is "+i+" is null->" +v);
}
}
System.out.println("At thread name is "+Thread.currentThread().getName()+" null. ");
}
}
</v.length;i++){
if(null==v){
isnull=true;
break;
//system.out.println("at>

双重检查的疑问 by fazhong tan

后面贴不上去!
static void print(String[] v){
boolean isNull=false;
for(int i=0;i<v.length;i++){
if(null==v){
isNull=true;
break;
}
}
System.out.println("At thread name is "+Thread.currentThread().getName()+" null. ");
}>

双重检查的疑问 by fazhong tan

贴不上去
如果非得用双重检查,则可以这样,那位兄弟还有什么高见??????
package com.thread.self.singleInstance;

import java.util.concurrent.CountDownLatch;
//安全发布
public class SingInstance3 {
private static String[] values;
private static CountDownLatch cdl=new CountDownLatch(1);//使用闭锁防止不安全发布
public static String[] getInstance() throws InterruptedException{
if(null==values){
synchronized(SingInstance2.class){
if(null==values){
values=new String[1000001];
int num=1000000;
for(int i=1000000;i>0;i--){
int va=i*1000;
values[num--]=new Integer(i).toString()+Thread.currentThread().getName()+","+va;
}
values[0]="endend";
cdl.countDown();
}
}
}
cdl.await();
return values;
}
}

Re: 双重检查的疑问 by 程 晓明

谢谢您的关注。

您的这个程序违反了双重检查锁定的隐含条件。

在参考文献1,参考文献4的16.2.4和参考文献5的71条,以及本文的双重检查锁定的示例代码,都使用类似如下的写法:
instance = new Instance();
这行代码包括三个子“操作”,它们在程序中的执行顺序如下:
1:分配instance引用的对象的内存空间
2:初始化对象
3:设置instance指向刚分配的内存地址
如果按上面的1->2->3来执行,双重检查锁定将是正确的。
换句话来说,如果按照程序的顺序来执行,双重检查锁定是正确的。--这是双重检查锁定的隐含条件。
由于JIT可能会对2和3重排序,所以双重检查锁定可能会出错。
声明instance为volatile后,禁止了2和3的重排序,程序将能保证正确性。

我们再来看您的程序。
您的程序的延迟初始化处理的程序顺序如下:
1:分配字符串数组的内存空间,并设置values指向刚分配的内存地址(values=new String[1000000];)
2:初始化字符串数组(values[num--]=...)
这个程序的程序顺序是先设置values,后初始化对象。
这个程序的程序顺序已经违反了双重检查锁定的隐含条件。
即使没有发生重排序,完全按照程序的顺来执行,线程也可能看到一个“没有内容”的字符串数组。

请教一个问题 by guo mike

一个变量声明volatile后,就不会被放到cpu 寄存器,cache中,而是主存中,这个观点是否正确?

线程的working memory,除了线程栈还有什么?working memory可能使用到寄存器,cache,主存,那么可以使用多大的主存呢?是否有这方面的资料。

请教一个问题 by guo mike

一个变量声明volatile后,就不会被放到cpu 寄存器,cache中,而是主存中,这个观点是否正确?

线程的working memory,除了线程栈还有什么?working memory可能使用到寄存器,cache,主存,那么可以使用多大的主存呢?是否有这方面的资料。

请教一个问题 by guo mike

一个变量声明volatile后,就不会被放到cpu 寄存器,cache中,而是主存中,这个观点是否正确?

线程的working memory,除了线程栈还有什么?working memory可能使用到寄存器,cache,主存,那么可以使用多大的主存呢?是否有这方面的资料。

Re: 请教一个问题 by Zhefu Zhou

请看该文作者的另一个系列文章:
www.infoq.com/cn/articles/java-memory-model-4?u...

Re: 双重检查的疑问 by fazhong tan

谢谢您的回复与分析!
您的分析让我更加深刻地理解了什么是指令排序、以及排序与寄存器、缓存、内存之间、JMM;
祝你快乐

求教延迟初始化的应用场景 by 崔 东

“延迟初始化降低了初始化类或创建实例的开销,但增加了访问被延迟初始化的字段的开销。在大多数时候,正常的初始化要优于延迟初始化”
我很赞成作者最后的总结,但是我实在是不知道对象延迟初始化在何种场景下应用。我的理解是既然系统中我们需要的对象早晚都要用,那为什么要多等那么一会,也就如作者最后说的多等这么一会的开销是很大的。

Re: 请教一个问题 by 程 晓明

不好意思,回复晚了。

一个变量声明volatile后,就不会被放到cpu 寄存器,cache中,而是主存中,这个观点是否正确?
--从JDK5开始,java使用JSR-133内存模型。对于JSR-133内存模型来说,这个观点不正确。
在JSR-133内存模型中,如果在单线程程序中使用volatile变量,这个volatile变量可能会被当做一个普通变量来对待。
在《深入理解Java内存模型(二)——重排序》的讨论中,对这个主题有详细的说明,可以供您参考
www.infoq.com/cn/articles/java-memory-model-2)
另外,在《深入理解Java内存模型(七)——总结》的“JMM的设计”中,对这个主题也有说明。
www.infoq.com/cn/articles/java-memory-model-7)

------------------------------------

线程的working memory,除了线程栈还有什么?working memory可能使用到寄存器,cache,主存,那么可以使用多大的主存呢?是否有这方面的资料。
--正如前面所言,从JDK5开始,java使用JSR-133内存模型。
在JSR-133内存模型中,根本就没有working memory的概念。《The Java Language Specification Third Edition》和《The Java Language Specification Java SE 7 Edition》对线程和内存模型的描述,都是复述自JSR-133内存模型规范。

working memory的概念来自于《JVM规范 第二版》的8.1章。《JVM规范 第二版》描述的是旧的java内存模型。从JDK5开始,这些描述已经不适用了。

Re: 求教延迟初始化的应用场景 by 程 晓明

延迟初始化的好处是:降低了初始化类或创建实例的开销。
延迟初始化的坏处是:增加了访问被延迟初始化的字段的开销。
是否应该使用延迟初始化,取决于这两方面的对比与权衡。
如果对象的初始化的开销非常昂贵,而且多个线程不会频繁调用getInstance()来获取被延迟初始化的对象时,可以考虑使用延迟初始化。

Re: 求教延迟初始化的应用场景 by 程 晓明

延迟初始化的好处是:降低了初始化类或创建实例的开销。
延迟初始化的坏处是:增加了访问被延迟初始化的字段的开销。
是否应该使用延迟初始化,取决于这两方面的对比与权衡。
如果对象的初始化的开销非常昂贵,而且多个线程不会频繁调用getInstance()来获取被延迟初始化的对象时,可以考虑使用延迟初始化。

Re: 求教延迟初始化的应用场景 by 崔 东

好吧,但是我总是觉得,几乎没有适合延迟初始化的场景。

既然对象总之要用,那也就是说早晚对象都会被初始化,那么开销早晚是会有的,与其这样还不如在应用启动的时候初始化。除非,这个对象有可能不会用到。

还是非常谢谢您分享的文章和回复。

JDK5之后不需要volatile关键字保证内存可见性? by Lu Zoe

请问是否可以理解为JSR-133内存模型已经保证了内存可见性,不会出现下面情况:一个线程修改的内容其他线程不实时可见?那volatile关键字的作用就只是禁止编译器或jvm对指令重排序?volatile还有什么其他应用场景?

Re: JDK5之后不需要volatile关键字保证内存可见性? by Lu Zoe

根据这篇文章JSR-133 FAQ , volatile是可以用来保证写操作后变量值are flushed out of the cache to main memory

Re: 求教延迟初始化的应用场景 by 程 晓明

其实把被延迟初始化的对象理解为某种资源(比如数据库连接),可能会更容易理解延迟初始化的应用场景。
如果获取或创建某种资源的开销非常昂贵,并且系统的资源有限且要求苛刻时,可以考虑延迟获取或延迟创建资源。

在《Pattern-Oriented Software Architecture Volume 3: Patterns for Resource Management》的2.2章(lazy acquisition模式)中,举了一个医疗影像归档和通信系统(Picture Archiving and Communication System, PACS)的例子来说明延迟获取资源模式的应用场景。

Re: JDK5之后不需要volatile关键字保证内存可见性? by 程 晓明

JSR-133只保证单线程和正确同步(这里的同步指的是广义上的同步,包括对lock,volatile,final的正确使用)的多线程程序的内存可见性。
在JSR-133中,未同步或未正确同步的多线程程序可能会发生内存可见性问题。

在JSR-133中,volatile并未完全禁止编译器和处理器的重排序,它只是禁止了那些“可能有害”的重排序。详情见:www.infoq.com/cn/articles/java-memory-model-4

volatile的应用场景:www.ibm.com/developerworks/java/library/j-jtp06...

Re: 求教延迟初始化的应用场景 by 崔 东

好的 ,谢谢,一会我去看看那篇文章,估计看到例子我会理解的更深刻

使用未被初始化的对象的引用会有什么异常 by 唐 名之

你好请问,在双重机制检查中,读到引用不为null,但对象却未被初始化,在使用这个引用会有什么异常?我想知道!谢谢!
emory = allocate(); //1:分配对象的内存空间
instance = memory; //3:设置instance指向刚分配的内存地址
//注意,此时对象还没有被初始化!
//比如在这行使用instance会有什么异常???最好能给点代码测试
ctorInstance(memory); //2:初始化对象

文章的疑问 by cong qingbin

问个问题:文章后面提到的类初始化过程,如果按照你说的步骤执行的话:所有的类初始化都必须获得LC,在初始化完成后再释放LC,那么A线程在执行初始化的过程中直到执行完毕,b线程都不会获得类的LC锁。也不会在获得锁后在condition中等待。

文章的疑问 by cong qingbin

问个问题:文章后面提到的类初始化过程,如果按照你说的步骤执行的话:所有的类初始化都必须获得LC,在初始化完成后再释放LC,那么A线程在执行初始化的过程中直到执行完毕,b线程都不会获得类的LC锁。也不会在获得锁后在condition中等待。

Re: 文章的疑问 by cong qingbin

哦,明白了,后面已经解释了,线程a会有两次获取锁的过程,在更改initial的过程中会有锁的释放。不好意思打搅了

两种延迟初始化的应用场景是什么? by Zhan Calvin

"如果确实需要对实例字段使用线程安全的延迟初始化,请使用上面介绍的基于volatile的延迟初始化的方案;如果确实需要对静态字段使用线程安全的延迟初始化,请使用上面介绍的基于类初始化的方案". 对这句话不是很了解. 是指类里面有实例字段和静态字段的意思吗? 能具体讲讲?

关于DCL的一点疑问 by 王 伟

就作者本文的例子而言,

public class DoubleCheckedLocking { //1
private static Instance instance; //2

public static Instance getInstance() { //3
if (instance == null) { //4:第一次检查
synchronized (DoubleCheckedLocking.class) { //5:加锁
if (instance == null) //6:第二次检查
instance = new Instance(); //7:问题的根源出在这里
} //8
} //9
return instance; //10
} //11
} //12

在第7处的伪代码:
memory = allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance = memory; //3:设置instance指向刚分配的内存地址

问题1.为什么使用了 volatile 关键字,在多线程的环境下,就能确保2,3不会重排序?
之前也读到作者一篇关于 volatile 介绍的文章,我个人理解,这里使用volatile 关键字 只能确保 线程A 对 Instance 的初始化 不能重排序 在线程 B 对Instance 的引用 后面。
这样也能保证 DCL正常工作,希望作者回答。
bset regard!

有个疑问 by lee desheng

blog.csdn.net/axman/article/details/1089196
不知你对这篇文章里提到内容有什么样的看法?希望您能抽空解答,谢谢!



文章中的这一段内容:
===========
确实,在JAVA2(以jdk1.2开始)以前对于实例字段是直接在主储区读写的.所以当一个线程对resource进行分配空间,
初始化和调用构造方法时,可能在其它线程中分配空间动作可见了,而初始化和调用构造方法还没有完成.
但是从JAVA2以后,JMM发生了根本的改变,分配空间,初始化,调用构造方法只会在线程的工作存储区完成,在没有
向主存储区复制赋值时,其它线程绝对不可能见到这个过程.而这个字段复制到主存区的过程,更不会有分配空间后
没有初始化或没有调用构造方法的可能.在JAVA中,一切都是按引用的值复制的.向主存储区同步其实就是把线程工作
存储区的这个已经构造好的对象有压缩堆地址值COPY给主存储区的那个变量.这个过程对于其它线程,要么是resource
为null,要么是完整的对象.绝对不会把一个已经分配空间却没有构造好的对象让其它线程可见

这样写是不是就不需要double check,并且没有安全性问题? by 何 清宝

public class Instance {
private static Instance instance = new Instance();

public static Instance getInstance() {
return instance;
}
}

长见识了 by Yuan Randy

感谢 。我也想到了双重检测。
但是没有考虑到代码重排序。
长见识了。对代码重排序还不够敏感。

Re: 还有个方法 by Zhang Gavin

编译器会优化掉

Re: 使用未被初始化的对象的引用会有什么异常 by Zhang Gavin

可能解释:
public class Clz {
private int size = 10;
}

不为 null 但还没有初始化完的情况下, 这个 size 可能就是 0.

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

38 讨论
提供反馈
错误报告
商务合作
内容合作
Marketing
InfoQ.com及所有内容,版权所有 © 2006-2016 C4Media Inc. InfoQ.com 服务器由 Contegix提供, 我们最信赖的ISP伙伴。
北京创新网媒广告有限公司 京ICP备09022563号-7 隐私政策
BT

We notice you’re using an ad blocker

We understand why you use ad blockers. However to keep InfoQ free we need your support. InfoQ will not provide your data to third parties without individual opt-in consent. We only work with advertisers relevant to our readers. Please consider whitelisting us.