`
xiaoxiao_0311
  • 浏览: 23560 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

单例模式的双重检查锁原理及JIT的无序写入对双重检查锁的破坏(上)

阅读更多

转载自http://www.ibm.com/developerworks/cn/java/j-dcl.html

单例创建模式是一个通用的编程习语。和多线程一起使用时,必需使用某种类型的同步。在努力创建更有效的代码时,Java程序员们创建了双重检查锁定习语,将其和单例创建模式一起使用,从而限制同步代码量。然而,由于一些不太常见的Java内存模型细节的原因,并不能保证这个双重检查锁定习语有效。它偶尔会失败,而不是总失败。此外,它失败的原因并不明显,还包含Java内存模型的一些隐秘细节。这些事实将导致代码失败,原因是双重检查锁定难于跟踪。在本文余下的部分里,我们将详细介绍双重检查锁定习语,从而理解它在何处失效。

单例创建习语

要理解双重检查锁定习语是从哪里起源的,就必须理解通用单例创建习语,如清单1中的阐释:


清单1.单例创建习语

importjava.util.*;
classSingleton
{
privatestaticSingletoninstance;
privateVectorv;
privatebooleaninUse;

privateSingleton()
{
v=
newVector();
v.addElement(
newObject());
inUse=
true;
}

publicstaticSingletongetInstance()
{
if(instance==null)//1
instance=newSingleton();//2
returninstance;//3
}
}

此类的设计确保只创建一个Singleton对象。构造函数被声明为privategetInstance()方法只创建一个对象。这个实现适合于单线程程序。然而,当引入多线程时,就必须通过同步来保护getInstance()方法。如果不保护getInstance()方法,则可能返回Singleton对象的两个不同的实例。假设两个线程并发调用getInstance()方法并且按以下顺序执行调用:

  • 线程1调用getInstance()方法并决定instance//1处为null
  • 线程1进入if代码块,但在执行//2处的代码行时被线程2预占。
  • 线程2调用getInstance()方法并在//1处决定instancenull
  • 线程2进入if代码块并创建一个新的Singleton对象并在//2处将变量instance分配给这个新对象。
  • 线程2//3处返回Singleton对象引用。
  • 线程2被线程1预占。
  • 线程1在它停止的地方启动,并执行//2代码行,这导致创建另一个Singleton对象。
  • 线程1//3处返回这个对象。

结果是getInstance()方法创建了两个Singleton对象,而它本该只创建一个对象。通过同步getInstance()方法从而在同一时间只允许一个线程执行代码,这个问题得以改正,如清单2所示:


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

publicstaticsynchronizedSingletongetInstance()
{
if(instance==null)//1
instance=newSingleton();//2
returninstance;//3
}

 

清单2中的代码针对多线程访问getInstance()方法运行得很好。然而,当分析这段代码时,您会意识到只有在第一次调用方法时才需要同步。由于只有第一次调用执行了//2处的代码,而只有此行代码需要同步,因此就无需对后续调用使用同步。所有其他调用用于决定instance是非null的,并将其返回。多线程能够安全并发地执行除第一次调用外的所有调用。尽管如此,由于该方法是synchronized的,需要为该方法的每一次调用付出同步的代价,即使只有第一次调用需要同步。

为使此方法更为有效,一个被称为双重检查锁定的习语就应运而生了。这个想法是为了避免对除第一次调用外的所有调用都实行同步的昂贵代价。同步的代价在不同的JVM间是不同的。在早期,代价相当高。随着更高级的JVM的出现,同步的代价降低了,但出入synchronized方法或块仍然有性能损失。不考虑JVM技术的进步,程序员们绝不想不必要地浪费处理时间。

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


清单3.getInstance()方法

publicstaticSingletongetInstance()
{
if(instance==null)
{
synchronized(Singleton.class){
instance=
newSingleton();
}
}
returninstance;
}

清单3中的代码展示了用多线程加以说明的和清单1相同的问题。当instancenull时,两个线程可以并发地进入if语句内部。然后,一个线程进入synchronized块来初始化instance,而另一个线程则被阻断。当第一个线程退出synchronized块时,等待着的线程进入并创建另一个Singleton对象。注意:当第二个线程进入synchronized块时,它并没有检查instance是否非null

双重检查锁定

为处理清单3中的问题,我们需要对instance进行第二次检查。这就是双重检查锁定名称的由来。将双重检查锁定习语应用到清单3的结果就是清单4


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

publicstaticSingletongetInstance()
{
if(instance==null)
{
synchronized(Singleton.class){//1
if(instance==null)//2
instance=newSingleton();//3
}
}
returninstance;
}

双重检查锁定背后的理论是:在//2处的第二次检查使(如清单3中那样)创建两个不同的Singleton对象成为不可能。假设有下列事件序列:

  • 线程1进入getInstance()方法。
  • 由于instancenull,线程1//1处进入synchronized块。
  • 线程1被线程2预占。
  • 线程2进入getInstance()方法。
  • 由于instance仍旧为null,线程2试图获取//1处的锁。然而,由于线程1持有该锁,线程2//1处阻塞。
  • 线程2被线程1预占。
  • 线程1执行,由于在//2处实例仍旧为null,线程1还创建一个Singleton对象并将其引用赋值给instance
  • 线程1退出synchronized块并从getInstance()方法返回实例。
  • 线程1被线程2预占。
  • 线程2获取//1处的锁并检查instance是否为null
  • 由于instance是非null的,并没有创建第二个Singleton对象,由线程1创建的对象被返回。

双重检查锁定背后的理论是完美的。不幸地是,现实完全不同。双重检查锁定的问题是:并不能保证它会在单处理器或多处理器计算机上顺利运行。

双重检查锁定失败的问题并不归咎于JVM中的实现bug,而是归咎于Java平台内存模型。内存模型允许所谓的无序写入,这也是这些习语失败的一个主要原因。

无序写入

为解释该问题,需要重新考察上述清单4中的//3行。此行代码创建了一个Singleton对象并初始化变量instance来引用此对象。这行代码的问题是:在Singleton构造函数体执行之前,变量instance可能成为非null的。

什么?这一说法可能让您始料未及,但事实确实如此。在解释这个现象如何发生前,请先暂时接受这一事实,我们先来考察一下双重检查锁定是如何被破坏的。假设清单4中代码执行以下事件序列:

  • 线程1进入getInstance()方法。
  • 由于instancenull,线程1//1处进入synchronized块。
  • 线程1前进到//3处,但在构造函数执行之前,使实例成为非null
  • 线程1被线程2预占。
  • 线程2检查实例是否为null。因为实例不为null,线程2instance引用返回给一个构造完整但部分初始化了的Singleton对象。
  • 线程2被线程1预占。
  • 线程1通过运行Singleton对象的构造函数并将引用返回给它,来完成对该对象的初始化。

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

为展示此事件的发生情况,假设为代码行instance=newSingleton();执行了下列伪代码:instance=newSingleton();

mem=allocate();//AllocatememoryforSingletonobject.
instance=mem;//Notethatinstanceisnownon-null,but
//hasnotbeeninitialized.
ctorSingleton(instance);//InvokeconstructorforSingletonpassing
//instance.

这段伪代码不仅是可能的,而且是一些JIT编译器上真实发生的。执行的顺序是颠倒的,但鉴于当前的内存模型,这也是允许发生的。JIT编译器的这一行为使双重检查锁定的问题只不过是一次学术实践而已。

为说明这一情况,假设有清单5中的代码。它包含一个剥离版的getInstance()方法。我已经删除了双重检查性以简化我们对生成的汇编代码(清单6)的回顾。我们只关心JIT编译器如何编译instance=newSingleton();代码。此外,我提供了一个简单的构造函数来明确说明汇编代码中该构造函数的运行情况。


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

classSingleton
{
privatestaticSingletoninstance;
privatebooleaninUse;
privateintval;

privateSingleton()
{
inUse=
true;
val=5;
}
publicstaticSingletongetInstance()
{
if(instance==null)
instance=
newSingleton();
returninstance;
}
}

清单6包含由SunJDK1.2.1JIT编译器为清单5中的getInstance()方法体生成的汇编代码。


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

;asmcodegeneratedforgetInstance
054D20B0moveax,[049388C8];loadinstanceref
054D20B5testeax,eax;test
fornull
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

汇编代码是通过运行一个在无限循环中调用getInstance()方法的测试程序来生成的。程序运行时,请运行MicrosoftVisualC++调试器并将其附到表示测试程序的Java进程中。然后,中断执行并找到表示该无限循环的汇编代码。

B0B5处的前两行汇编代码将instance引用从内存位置049388C8加载至eax中,并进行null检查。这跟清单5中的getInstance()方法的第一行代码相对应。第一次调用此方法时,instancenull,代码执行到B9BE处的代码为Singleton对象从堆中分配内存,并将一个指向该块内存的指针存储到eax中。下一行代码,C3,获取eax中的指针并将其存储回内存位置为049388C8的实例引用。结果是,instance现在为非null并引用一个有效的Singleton对象。然而,此对象的构造函数尚未运行,这恰是破坏双重检查锁定的情况。然后,在C8行处,instance指针被解除引用并存储到ecxCAD0行表示内联的构造函数,该构造函数将值true5存储到Singleton对象。如果此代码在执行C3行后且在完成该构造函数前被另一个线程中断,则双重检查锁定就会失败。

不是所有的JIT编译器都生成如上代码。一些生成了代码,从而只在构造函数执行后使instance成为非null。针对Java技术的IBMSDK1.3版和SunJDK1.3都生成这样的代码。然而,这并不意味着应该在这些实例中使用双重检查锁定。该习语失败还有一些其他原因。此外,您并不总能知道代码会在哪些JVM上运行,而JIT编译器总是会发生变化,从而生成破坏此习语的代码。

双重检查锁定:获取两个

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


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

publicstaticSingletongetInstance()
{
if(instance==null)
{
synchronized(Singleton.class){//1
Singletoninst=instance;//2
if(inst==null)
{
synchronized(Singleton.class){//3
inst=newSingleton();//4
}
instance=inst;
//5
}
}
}
returninstance;
}

 

看着清单7中的代码,您应该意识到事情变得有点荒谬。请记住,创建双重检查锁定是为了避免对简单的三行getInstance()方法实现同步。清单7中的代码变得难于控制。另外,该代码没有解决问题。仔细检查可获悉原因。

此代码试图避免无序写入问题。它试图通过引入局部变量inst和第二个synchronized块来解决这一问题。该理论实现如下:

·         线程1进入getInstance()方法。

·         由于instancenull,线程1//1处进入第一个synchronized块。

·         局部变量inst获取instance的值,该值在//2处为null

·         由于instnull,线程1//3处进入第二个synchronized块。

·         线程1然后开始执行//4处的代码,同时使inst为非null,但在Singleton的构造函数执行前。(这就是我们刚才看到的无序写入问题。)

·         线程1被线程2预占。

·         线程2进入getInstance()方法。

·         由于instancenull,线程2试图在//1处进入第一个synchronized块。由于线程1目前持有此锁,线程2被阻断。

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

·         线程1然后将一个构造完整的Singleton对象在//5处赋值给变量instance,并退出这两个synchronized块。

·         线程1返回instance

·         然后执行线程2并在//2处将instance赋值给inst

·         线程2发现instance为非null,将其返回。

这里的关键行是//5。此行应该确保instance只为null或引用一个构造完整的Singleton对象。该问题发生在理论和实际彼此背道而驰的情况下。

由于当前内存模型的定义,清单7中的代码无效。Java语言规范(JavaLanguageSpecificationJLS)要求不能将synchronized块中的代码移出来。但是,并没有说不能将synchronized块外面的代码移synchronized块中。

JIT编译器会在这里看到一个优化的机会。此优化会删除//4//5处的代码,组合并且生成清单8中所示的代码。

分享到:
评论

相关推荐

    java代码-double check单例模式

    **Java代码 - 双重检查锁定(Double-Check Locking)单例模式** 在Java编程中,单例模式是一种常用的设计模式,它确保一个类只有一个实例,并提供一个全局访问点。这种模式在需要频繁创建和销毁对象的场景下非常...

    jvm初识及JIT优化

    jvm初识及JIT优化jvm初识及JIT优化jvm初识及JIT优化jvm初识及JIT优化jvm初识及JIT优化jvm初识及JIT优化jvm初识及JIT优化jvm初识及JIT优化jvm初识及JIT优化jvm初识及JIT优化jvm初识及JIT优化jvm初识及JIT优化jvm初识...

    JIT Spray技术.pdf

    即时编译喷射是一种技术,通过利用ActionScript即时编译器(JIT Compiler)的行为来将shellcode写入可执行内存,从而绕过DEP的限制。DEP旨在阻止非执行区域的代码执行,而JIT Spraying利用了JIT编译器在特定条件下将...

    Android Framework介绍 Dalvik原理 JIT介绍

    ### Android Framework介绍与Dalvik原理 #### 一、Android Framework概览 Android系统是一个复杂的软件堆栈,由多个层次组成,旨在为移动设备提供全面的支持。这些层包括Linux内核、中间层(如库和虚拟机)以及...

    C#JIT的编译原理

    关于C#编译原理,高级教程之一的指导教程。

    Trace-based JIT简介(对Method JIT的改进)

    ### Trace-Based JIT简介(对Method JIT的改进) #### 背景与动机 在现代软件开发领域,即时编译器(Just-In-Time Compiler, JIT)技术对于提高程序运行时性能至关重要,尤其是在Java这样的动态类型语言中。传统的...

    JIT采购模式市场营销论文.doc

    JIT采购模式市场营销论文.doc

    jit spray source code

    它利用了动态编译器(如JIT编译器)的工作原理,将大量恶意代码注入到内存中,以便在特定条件触发时执行。本篇文章将深入探讨JIT Spray的概念、工作原理以及相关的源代码分析。 ### JIT Spray简介 JIT Spray是一种...

    SAP可用性检查应用测试.docx

    在SAP系统中,可用性检查是一个至关重要的功能,它确保了企业在进行销售订单、预留、生产订单等操作时,能够准确评估物料的供应能力,防止出现库存短缺或过度承诺的情况。本文将深入探讨SAP的两种可用性检查类型:...

    erp与jit——案例分析:对mrp2与丰田管理模式(jit)的比较.doc

    丰田管理模式 erp与jit——案例分析

    jit JavaScript

    JIT的工作原理是,在程序运行期间,当某个函数或代码块被频繁调用时,JIT编译器会捕获这部分代码,对其进行优化,并将其转换为高效的机器代码。这样,下次执行该代码时,就可以直接使用编译后的版本,避免了解释执行...

    JIT营销模式运营和研究.doc

    综上所述,JIT营销模式是一种注重效率、灵活性和顾客需求的现代营销策略,它将电子商务、网络营销与传统销售方式有机融合,通过精确的品牌定位和全方位的服务,提升了企业的竞争力和市场份额。这种模式对于面临成长...

    深入探索Java JIT编译器:原理、优化与实践

    本文将详细介绍Java JIT编译器的工作原理、优化策略以及如何在实际开发中利用JIT提高程序性能。 Java JIT编译器是提高Java程序性能的关键技术之一。通过理解其工作原理和优化策略,开发者可以更好地利用JIT编译器来...

    java的JIT 工作原理简单介绍

    以下是对JIT工作原理和相关知识的详细解释: 1. **JIT编译过程**: 当Java程序运行时,JVM首先会解释执行字节码。这个过程类似于读取一个指令集,然后逐条执行。然而,解释执行的效率相对较低,因为它需要在运行时...

    专题资料(2021-2022年)JIT管理模式.doc

    三、我国基于JIT模式的生产计划系统的现状及个人看法 在国内,JIT管理模式已经被许多企业所采用,尤其在汽车行业。然而,我国企业在实施JIT时,面临基础设施、供应商能力、市场需求预测等方面的挑战。个人认为,我国...

    浅析dalvik虚拟机JIT技术的实现.doc

    本文旨在深入探讨Dalvik虚拟机中JIT技术的实现原理,以及其在Android平台上的应用。 #### Dalvik虚拟机与JIT技术概述 Dalvik虚拟机是专为Android设计的虚拟机,不同于传统的Java虚拟机(JVM),它采用寄存器架构而...

    JIT采购供应商管理模式.doc

    【JIT采购供应商管理模式】 JIT(Just In Time)采购供应商管理模式是一种先进的供应链管理策略,旨在消除浪费,提高效率,确保在需要时提供准确数量的产品。这种模式源于日本丰田公司的生产理念,它强调以客户需求...

    Writing JIT-Spray Shellcode for fun and profit

    编写适用于JIT-Spray的shellcode涉及到对底层硬件架构、编译原理及安全机制的深刻理解。首先,攻击者必须识别目标系统上运行的JIT编译器类型(如V8引擎用于Chrome,Chakra引擎用于Edge等)。接下来,需要设计能够被...

    JIT(Just In Time)采购知识-准时化采购

    1. 面向需求:JIT采购模式强调以需求为导向,不同于以往库存导向的采购模式。物料采购与需求同步进行,直接将物料送到生产线,从而避免产生过剩库存。这要求企业具备高度的预测能力和供应商的快速响应机制。 2. ...

Global site tag (gtag.js) - Google Analytics