双重检查锁定及单例模式Word文档下载推荐.docx

上传人:b****2 文档编号:1335186 上传时间:2023-04-30 格式:DOCX 页数:27 大小:28.62KB
下载 相关 举报
双重检查锁定及单例模式Word文档下载推荐.docx_第1页
第1页 / 共27页
双重检查锁定及单例模式Word文档下载推荐.docx_第2页
第2页 / 共27页
双重检查锁定及单例模式Word文档下载推荐.docx_第3页
第3页 / 共27页
双重检查锁定及单例模式Word文档下载推荐.docx_第4页
第4页 / 共27页
双重检查锁定及单例模式Word文档下载推荐.docx_第5页
第5页 / 共27页
双重检查锁定及单例模式Word文档下载推荐.docx_第6页
第6页 / 共27页
双重检查锁定及单例模式Word文档下载推荐.docx_第7页
第7页 / 共27页
双重检查锁定及单例模式Word文档下载推荐.docx_第8页
第8页 / 共27页
双重检查锁定及单例模式Word文档下载推荐.docx_第9页
第9页 / 共27页
双重检查锁定及单例模式Word文档下载推荐.docx_第10页
第10页 / 共27页
双重检查锁定及单例模式Word文档下载推荐.docx_第11页
第11页 / 共27页
双重检查锁定及单例模式Word文档下载推荐.docx_第12页
第12页 / 共27页
双重检查锁定及单例模式Word文档下载推荐.docx_第13页
第13页 / 共27页
双重检查锁定及单例模式Word文档下载推荐.docx_第14页
第14页 / 共27页
双重检查锁定及单例模式Word文档下载推荐.docx_第15页
第15页 / 共27页
双重检查锁定及单例模式Word文档下载推荐.docx_第16页
第16页 / 共27页
双重检查锁定及单例模式Word文档下载推荐.docx_第17页
第17页 / 共27页
双重检查锁定及单例模式Word文档下载推荐.docx_第18页
第18页 / 共27页
双重检查锁定及单例模式Word文档下载推荐.docx_第19页
第19页 / 共27页
双重检查锁定及单例模式Word文档下载推荐.docx_第20页
第20页 / 共27页
亲,该文档总共27页,到这儿已超出免费预览范围,如果喜欢就下载吧!
下载资源
资源描述

双重检查锁定及单例模式Word文档下载推荐.docx

《双重检查锁定及单例模式Word文档下载推荐.docx》由会员分享,可在线阅读,更多相关《双重检查锁定及单例模式Word文档下载推荐.docx(27页珍藏版)》请在冰点文库上搜索。

双重检查锁定及单例模式Word文档下载推荐.docx

}

publicstaticSingletongetInstance()

if(instance==null)//1

instance=newSingleton();

//2

returninstance;

//3

}

此类的设计确保只创建一个 

Singleton 

对象。

构造函数被声明为 

private,getInstance() 

方法只创建一个对象。

这个实现适合于单线程程序。

然而,当引入多线程时,就必须通过同步来保护 

getInstance()方法。

如果不保护 

getInstance() 

方法,则可能返回 

对象的两个不同的实例。

假设两个线程并发调用 

方法并且按以下顺序执行调用:

1.线程1调用 

方法并决定 

instance 

在//1处为 

null。

 

2.线程1进入 

if 

代码块,但在执行//2处的代码行时被线程2预占。

3.线程2调用 

方法并在//1处决定 

为 

4.线程2进入 

代码块并创建一个新的 

对象并在//2处将变量 

分配给这个新对象。

5.线程2在//3处返回 

对象引用。

6.线程2被线程1预占。

7.线程1在它停止的地方启动,并执行//2代码行,这导致创建另一个 

8.线程1在//3处返回这个对象。

结果是 

方法创建了两个 

对象,而它本该只创建一个对象。

通过同步 

方法从而在同一时间只允许一个线程执行代码,这个问题得以改正,如清单2所示:

清单2.线程安全的getInstance()方法

publicstaticsynchronizedSingletongetInstance()

清单2中的代码针对多线程访问 

方法运行得很好。

然而,当分析这段代码时,您会意识到只有在第一次调用方法时才需要同步。

由于只有第一次调用执行了//2处的代码,而只有此行代码需要同步,因此就无需对后续调用使用同步。

所有其他调用用于决定 

是非 

null 

的,并将其返回。

多线程能够安全并发地执行除第一次调用外的所有调用。

尽管如此,由于该方法是 

synchronized 

的,需要为该方法的每一次调用付出同步的代价,即使只有第一次调用需要同步。

为使此方法更为有效,一个被称为双重检查锁定的习语就应运而生了。

这个想法是为了避免对除第一次调用外的所有调用都实行同步的昂贵代价。

同步的代价在不同的JVM间是不同的。

在早期,代价相当高。

随着更高级的JVM的出现,同步的代价降低了,但出入 

方法或块仍然有性能损失。

不考虑JVM技术的进步,程序员们绝不想不必要地浪费处理时间。

因为只有清单2中的//2行需要同步,我们可以只将其包装到一个同步块中,如清单3所示:

清单3.getInstance()方法

publicstaticSingletongetInstance()

if(instance==null)

synchronized(Singleton.class){

清单3中的代码展示了用多线程加以说明的和清单1相同的问题。

当 

时,两个线程可以并发地进入 

语句内部。

然后,一个线程进入 

块来初始化 

instance,而另一个线程则被阻断。

当第一个线程退出 

块时,等待着的线程进入并创建另一个 

注意:

当第二个线程进入 

块时,它并没有检查 

是否非 

回页首

双重检查锁定

为处理清单3中的问题,我们需要对 

进行第二次检查。

这就是“双重检查锁定”名称的由来。

将双重检查锁定习语应用到清单3的结果就是清单4。

清单4.双重检查锁定示例

synchronized(Singleton.class){//1

if(instance==null)//2

双重检查锁定背后的理论是:

在//2处的第二次检查使(如清单3中那样)创建两个不同的 

对象成为不可能。

假设有下列事件序列:

1.线程1进入 

方法。

2.由于 

null,线程1在//1处进入 

块。

3.线程1被线程2预占。

5.由于 

仍旧为 

null,线程2试图获取//1处的锁。

然而,由于线程1持有该锁,线程2在//1处阻塞。

7.线程1执行,由于在//2处实例仍旧为 

null,线程1还创建一个 

对象并将其引用赋值给 

instance。

8.线程1退出 

块并从 

方法返回实例。

9.线程1被线程2预占。

10.线程2获取//1处的锁并检查 

是否为 

11.由于 

的,并没有创建第二个 

对象,由线程1创建的对象被返回。

双重检查锁定背后的理论是完美的。

不幸地是,现实完全不同。

双重检查锁定的问题是:

并不能保证它会在单处理器或多处理器计算机上顺利运行。

双重检查锁定失败的问题并不归咎于JVM中的实现bug,而是归咎于Java平台内存模型。

内存模型允许所谓的“无序写入”,这也是这些习语失败的一个主要原因。

无序写入

为解释该问题,需要重新考察上述清单4中的//3行。

此行代码创建了一个 

对象并初始化变量 

来引用此对象。

这行代码的问题是:

在 

构造函数体执行之前,变量 

instance可能成为非 

的。

什么?

这一说法可能让您始料未及,但事实确实如此。

在解释这个现象如何发生前,请先暂时接受这一事实,我们先来考察一下双重检查锁定是如何被破坏的。

假设清单4中代码执行以下事件序列:

3.线程1前进到//3处,但在构造函数执行之前,使实例成为非 

4.线程1被线程2预占。

5.线程2检查实例是否为 

因为实例不为null,线程2将 

引用返回给一个构造完整但部分初始化了的 

7.线程1通过运行 

对象的构造函数并将引用返回给它,来完成对该对象的初始化。

此事件序列发生在线程2返回一个尚未执行构造函数的对象的时候。

为展示此事件的发生情况,假设为代码行 

instance=newSingleton();

执行了下列伪代码:

mem=allocate();

//AllocatememoryforSingletonobject.

instance=mem;

//Notethatinstanceisnownon-null,but

//hasnotbeeninitialized.

ctorSingleton(instance);

//InvokeconstructorforSingletonpassing

//instance.

这段伪代码不仅是可能的,而且是一些JIT编译器上真实发生的。

执行的顺序是颠倒的,但鉴于当前的内存模型,这也是允许发生的。

JIT编译器的这一行为使双重检查锁定的问题只不过是一次学术实践而已。

为说明这一情况,假设有清单5中的代码。

它包含一个剥离版的 

我已经删除了“双重检查性”以简化我们对生成的汇编代码(清单6)的回顾。

我们只关心JIT编译器如何编译instance=newSingleton();

代码。

此外,我提供了一个简单的构造函数来明确说明汇编代码中该构造函数的运行情况。

清单5.用于演示无序写入的单例类

privateintval;

val=5;

清单6包含由SunJDK1.2.1JIT编译器为清单5中的 

方法体生成的汇编代码。

清单6.由清单5中的代码生成的汇编代码

;

asmcodegeneratedforgetInstance

054D20B0moveax,[049388C8];

loadinstanceref

054D20B5testeax,eax;

testfornull

054D20B7jne054D20D7

054D20B9moveax,14C0988h

054D20BEcall503EF8F0;

allocatememory

054D20C3mov[049388C8],eax;

storepointerin

;

instanceref.instance

non-nullandctor

hasnotrun

054D20C8movecx,dwordptr[eax]

054D20CAmovdwordptr[ecx],1;

inlinector-inUse=true;

054D20D0movdwordptr[ecx+4],5;

inlinector-val=5;

054D20D7movebx,dwordptrds:

[49388C8h]

054D20DDjmp054D20B0

注:

为引用下列说明中的汇编代码行,我将引用指令地址的最后两个值,因为它们都以 

054D20 

开头。

例如,B5 

代表 

testeax,eax。

汇编代码是通过运行一个在无限循环中调用 

方法的测试程序来生成的。

程序运行时,请运行MicrosoftVisualC++调试器并将其附到表示测试程序的Java进程中。

然后,中断执行并找到表示该无限循环的汇编代码。

B0 

和 

B5 

处的前两行汇编代码将 

引用从内存位置 

049388C8 

加载至 

eax 

中,并进行 

检查。

这跟清单5中的 

方法的第一行代码相对应。

第一次调用此方法时,instance 

为null,代码执行到 

B9。

BE 

处的代码为 

对象从堆中分配内存,并将一个指向该块内存的指针存储到 

中。

下一行代码,C3,获取 

中的指针并将其存储回内存位置为 

的实例引用。

结果是,instance 

现在为非 

并引用一个有效的 

然而,此对象的构造函数尚未运行,这恰是破坏双重检查锁定的情况。

然后,在 

C8 

行处,instance 

指针被解除引用并存储到 

ecx。

CA 

和D0 

行表示内联的构造函数,该构造函数将值 

true 

存储到 

如果此代码在执行 

C3 

行后且在完成该构造函数前被另一个线程中断,则双重检查锁定就会失败。

不是所有的JIT编译器都生成如上代码。

一些生成了代码,从而只在构造函数执行后使 

成为非 

针对Java技术的IBMSDK1.3版和SunJDK1.3都生成这样的代码。

然而,这并不意味着应该在这些实例中使用双重检查锁定。

该习语失败还有一些其他原因。

此外,您并不总能知道代码会在哪些JVM上运行,而JIT编译器总是会发生变化,从而生成破坏此习语的代码。

双重检查锁定:

获取两个

考虑到当前的双重检查锁定不起作用,我加入了另一个版本的代码,如清单7所示,从而防止您刚才看到的无序写入问题。

清单7.解决无序写入问题的尝试

Singletoninst=instance;

if(inst==null)

synchronized(Singleton.class){//3

inst=newSingleton();

//4

instance=inst;

//5

看着清单7中的代码,您应该意识到事情变得有点荒谬。

请记住,创建双重检查锁定是为了避免对简单的三行 

方法实现同步。

清单7中的代码变得难于控制。

另外,该代码没有解决问题。

仔细检查可获悉原因。

此代码试图避免无序写入问题。

它试图通过引入局部变量 

inst 

和第二个 

块来解决这一问题。

该理论实现如下:

null,线程1在//1处进入第一个 

3.局部变量 

获取 

的值,该值在//2处为 

4.由于 

null,线程1在//3处进入第二个 

5.线程1然后开始执行//4处的代码,同时使 

为非 

null,但在 

的构造函数执行前。

(这就是我们刚才看到的无序写入问题。

) 

6.线程1被线程2预占。

7.线程2进入 

8.由于 

null,线程2试图在//1处进入第一个 

由于线程1目前持有此锁,线程2被阻断。

9.线程1然后完成//4处的执行。

10.线程1然后将一个构造完整的 

对象在//5处赋值给变量 

instance,并退出这两个 

11.线程1返回 

12.然后执行线程2并在//2处将 

赋值给 

inst。

13.线程2发现 

null,将其返回。

这里的关键行是//5。

此行应该确保 

只为 

或引用一个构造完整的 

该问题发生在理论和实际彼此背道而驰的情况下。

由于当前内存模型的定义,清单7中的代码无效。

Java语言规范(JavaLanguageSpecification,JLS)要求不能将 

块中的代码移出来。

但是,并没有说不能将 

块外面的代码移入 

块中。

JIT编译器会在这里看到一个优化的机会。

此优化会删除//4和//5处的代码,组合并且生成清单8中所示的代码。

清单8.从清单7中优化来的代码。

//inst=newSingleton();

//instance=inst;

如果进行此项优化,您将同样遇到我们之前讨论过的无序写入问题。

用volatile声明每一个变量怎么样?

另一个想法是针对变量 

以及 

使用关键字 

volatile。

根据JLS(参见 

参考资料),声明成 

volatile 

的变量被认为是顺序一致的,即,不是重新排序的。

但是试图使用 

来修正双重检查锁定的问题,会产生以下两个问题:

∙这里的问题不是有关顺序一致性的,而是代码被移动了,不是重新排序。

∙即使考虑了顺序一致性,大多数的JVM也没有正确地实现 

第二点值得展开讨论。

假设有清单9中的代码:

清单9.使用了volatile的顺序一致性

classtest

privatevolatilebooleanstop=false;

privatevolatileintnum=0;

publicvoidfoo()

num=100;

//Thiscanhappensecond

stop=true;

//Thiscanhappenfirst

//...

publicvoidbar()

if(stop)

num+=num;

//numcan==0!

根据JLS,由于 

stop 

num 

被声明为 

volatile,它们应该顺序一致。

这意味着如果 

曾经是 

true,num 

一定曾被设置成 

100。

尽管如此,因为许多JVM没有实现 

的顺序一致性功能,您就不能依赖此行为。

因此,如果线程1调用 

foo 

并且线程2并发地调用 

bar,则线程1可能在 

被设置成为 

100 

之前将 

设置成 

true。

这将导致线程见到 

是 

true,而 

仍被设置成 

0。

使用volatile 

和64位变量的原子数还有另外一些问题,但这已超出了本文的讨论范围。

有关此主题的更多信息,请参阅 

参考资料。

解决方案

底线就是:

无论以何种形式,都不应使用双重检查锁定,因为您不能保证它在任何JVM实现上都能顺利运行。

JSR-133是有关内存模型寻址问题的,尽管如此,新的内存模型也不会支持双重检查锁定。

因此,您有两种选择:

∙接受如清单2中所示的 

方法的同步。

∙放弃同步,而使用一个 

static 

字段。

选择项2如清单10中所示

清单10.使用static字段的单例实现

priv

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 小学教育 > 语文

copyright@ 2008-2023 冰点文库 网站版权所有

经营许可证编号:鄂ICP备19020893号-2