`
阅读更多
       众所周知单例模式有有饿汉式与懒汉式两种。当一个单例类的初始化开销很大,而希望当用户实际上需要的时候才去创建单例类,就会考虑使用懒汉式延迟初始化,来提高程序的启动速度。但懒汉式并不容易使用。
在多线程的环境下,如果不同步getInstance()方法会出现线程安全的问题,如果同步整个方法,那么getInstance()就完全变成串行,串行效率会降低10倍甚至100倍。因此,有些聪明的程序员就把C中常用的DCL(double-checked locking,中文名双重检查加锁)搬到JAVA上来,看上去很强大。如下:
双重检查加锁
public class Singleton {
     private static Singleton instance;
     
     //私有构造函数
     private Singleton(){
    	 
     }
     
     //双重检查加锁的代码
     public static Singleton getInstance(){
    	 if(instance==null){
    		 synchronized(Singleton.class){      //1
    			 if(instance==null){             //2
    				 instance = new Singleton(); //3
    			 }
    		 }
    	 }
    	 return instance;
     }
}


引用IBM-中国网站上的一段话按DCL(双重检查加锁)的理论进行假设

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

2.由于 instance 为 null,线程 1 在 //1 处进入 synchronized 块。

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

4.线程 2 进入 getInstance() 方法。

5.由于 instance 仍旧为 null,线程 2 试图获取 //1 处的锁。然而,由于线程 1 持有该锁,线程 2 在 //1 处阻塞。

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

7.线程 1 执行,由于在 //2 处实例仍旧为 null,线程 1 还创建一个 Singleton 对象并将其引用赋值给 instance。

8.线程 1 退出 synchronized 块并从 getInstance() 方法返回实例。

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

10.线程 2 获取 //1 处的锁并检查 instance 是否为 null。

11.由于 instance 是非 null 的,并没有创建第二个 Singleton 对象,由线程 1 创建的对象被返回。

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

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

详情资料可以查看:http://www.ibm.com/developerworks/cn/java/j-dcl.html


上面也说了,究其原因是Java平台内存模型(JMM)--
这里简单介绍一下Java的内存模型:
        不像大多数其它语言,Java定义了它和潜在硬件的关系,通过一个能运行所有Java平台的正式内存模型,能够实现Java“写一次,到处运行”的诺言。通过比较,其它语言像C和C++,缺乏一个正式的内存模型;在这些语言中,程序继承了运行该程序硬件平台的内存模型。

         当运行在同步(单线程)环境中,一段程序与内存交互相当的简单,或者说至少它的表现如此。程序存贮条目到内存位置上,并且在下一次这些内存位置被检测的时候期望它们仍然在那儿。

         实际上,原理是完全不同的。但是通过编译器,Java虚拟机(JVM)维持着一个复杂的幻想,并且硬件把它掩饰起来。虽然,我们认为程序将像程序代码中说明的顺序连续执行,但是事情不是总是这样发生的。编译器,处理器和缓存可以自由的随意的应用我们的程序和数据,只要它们不会影响到计算的结果。例如,编译器能用不同的顺序从明显的程序解释中生成指令,并且存贮变量在寄存器中,而不是内存中;处理器可以并行或者颠倒次序的执行指令缓存可以改变顺序的把提交内容写入到主存中。Java内存模型(JMM)所说的只要环境维持了as-if-serial语法,所有的各种各样的再排序和优化是可以接受的。也就是说,只要你完成了同样的结果与你在一个严格的连续环境中指令被执行的结果一样。
 
          编译器,处理器和缓存为了达到高性能,需要重新安排程序运行的顺序。近年来,我们看到了计算机的计算性能有了极大的提高。处理器时钟频率的提高对高性能有着充分的贡献,并行能力(管道的形式和超标量体系结构执行单元,动态指令分配表和灵活的执行,精密的多级内存缓存)的提高也是主要的贡献者。当今,编写编译器的任务变得极其复杂,因为在这些复杂性中,编译器必须能保护程序员。

          当编写单线程程序时,你不会看到这些多种多样指令或者内存重新排序运行的结果。然而,在多线程程序中,情况则完全不同——一个线程可以读另一个线程已经写了的内存位置。如果线程A用某一种顺序修改了一些变量,在缺乏同步时,线程B可能用同样的顺序也看不到它们,或者根本看不到它们。那可能是由于编译器把指令重新排序或临时在寄存器中存储了变量,后来把它写到内存以外;或者是由于处理器并行的或与编译器指定的不同的顺序执行指令;或者是由于指令在内存的不同区域,而且缓存用与它们写进时不一样的顺序更新了相应的主存位置。无论何种环境,多线程程序先天的缺乏可预见性。除非你通过使用同步明确地保证线程有一个一致的内存视图。


          阎宏在《Java与模式》也有精辟的总结:在Java 编译器中,Singleton 类的初始化与instance变量赋值的顺序不可预料。如果一个线程在没有同步化的条件下读取instance 引用,并调用这个对象的方法的话,可能会发现对象的初始化过程尚未完成,从而造成崩溃。


         由于JMM天生的无序性,导致了在Singleton构造函数执行之前,变量instance可能成为非null !!
假设线程1执行到
instance = new Singleton();
这一句,但又未完成初始化时,被线程抢夺掉时间片,这时线程2判断了instance非空并返回一个instance对象,但好可惜,这只是一个未完成初始化的半成品!这个半成品的成员很可能是失效的。


举一个比较贴近实际的例子
public class Test4 {
      public static void main(String[] args) {
		 X x = new X();
	}
}
class X {
	static String name = "rjx";

	static {
		System.out.println("---------运行静态块的代码------------");
		name = "Jam";
	}

	{
		System.out.println("--------运行实例块/初始化块的代码--------");
	}

	public X() {
		System.out.println("--------运行构造函数的代码-------");
		System.out.println("静态变量name现在的值是" + name);
	}
}

结果显示:





这段代码的结果相信大部分朋友都很清楚,可以得知一般程序的运行程序是:静态代码块/静态变量-->实例块/实例变量-->构造函数。
但如果将代码稍微改变下:
public class Test4 {
      public static void main(String[] args) {
		 X x = new X();
	}
}
class X {
	//增加自己的静态实例变量
	static X x = new X();
	
	static String name = "rjx";

	static {
		System.out.println("---------运行静态块的代码------------");
		name = "Jam";
	}

	{
		System.out.println("--------运行实例块/初始化块的代码--------");
	}

	public X() {
		System.out.println("--------运行构造函数的代码-------");
		System.out.println("静态变量name现在的值是" + name);
	}
}

结果显示:




结果可能有些出乎你的意料。name中间的值为null,这就是之前所说的失效值
返回上面的双重检查加锁的单例程序,因为并非整个getInstance()的是同步的。一个线程当执行getInstance()的时候,JVM分配内存,分配Singleton,调用构造函数。在完成分配内存并且instance字段已经设置,但构造函数还没调用之时,被另外一个线程抢夺了时间片,这时它进行getInstance并且进行非空判断,它认为instance不为空,跳过synchronized快,返回一个部分构造的instance对象!不必说,这个结果既不是我们期望的,也不是我们想象的。


使用Volatile关键字能解决问题但不可取
《Head First设计模式》中提倡在Java 5的环境下用Volatile修饰instance变量,试图使instance变量被认为是顺序一致,不是重新排序的。因为Java 5更改了Volatile的语义,使Volatile变量的读写与syncrhonized有相当的含义。这样做虽然能使DCL的功能正确,效率却和在 synchronized block 内部单一检查相当,甚至更差一点(因为检查了两次)。在我看来,DCL 已经完全不具有物理意义了。而且大多数的JVM也没有正确地实现Volatile。


==============================================无敌分界线==============================================


        貌似只有同步整个方法和使用饿汉式才能解决问题之际,一种全新的思想被提出来,并马上得到了大家的认可
请看下面的代码
public class Singleton {

     private Singleton(){
    	 
     }
     //静态内部类
     static class SingletonInner{
    	static Singleton instance = new Singleton();
     }
     
     public static Singleton getInstance(){
    	 return SingletonInner.instance;
     }
}

使用静态内部类!它是由一个叫Bob Lee的人写下来的(最初忘记了是哪两个人提出)。在加载singleton时并不加载它的内部类SingletonInner,而在调用getInstance()时调用SingletonInner时才加载SingletonInner,从而调用singleton的构造函数,实例化singleton,从而在不需要同步的情况下,达到延迟初始化的效果。 
  • 大小: 14.1 KB
  • 大小: 8.3 KB
分享到:
评论
4 楼 沧之云 2014-02-27  
  看了后面很多字,但并没有说明为什么双重判空行不通,文章(后面n个例子代码)说的只是jvm对类加载的顺序而已,而jvm对类加载的顺序,每个jvm都有细微区别的。
  可惜的是,文章原本想表示双重判空的不可行性,但不知道发文前,是否在例子中加一个sayHi的方法,并输出成员静态的 x 是否为null? (这可是标准的双重判空写法,因为基本没人在getInstance()写在构造方法里吧)
  
3 楼 softbear 2010-01-02  
这种方法很好,谢谢了
2 楼 commonleoj 2009-08-23  
不错...
1 楼 orange_09 2009-03-04  

相关推荐

    工厂模式与单例模式

    在软件设计模式中,工厂模式和单例模式是两种非常基础且重要的模式,它们都是用于解决对象创建问题,但有着不同的设计理念和应用场景。本篇文章将深入探讨这两种模式,并结合具体的代码示例`myFactoryDemo`进行讲解...

    C++单例模式的实例详解

    个人认为单例模式是设计模式中最为简单、最为常见、最容易实现,也是最应该熟悉和掌握的模式。且不说公司企业在招聘的时候为了考察员工对设计的了解和把握,考的最多的就是单例模式。 单例模式解决问题十分常见,...

    详解Ruby设计模式编程中对单例模式的运用

    ### 详解Ruby设计模式编程中对单例模式的运用 #### 概述 单例模式是一种常见的设计模式,它的核心思想在于确保一个类只有一个实例,并且这...此外,理解类变量与实例变量的区别有助于更好地掌握单例模式的内部机制。

    ExtJS 设计模式之一.docx

    ### ExtJS 单例设计模式解析 #### 一、引言 随着Web应用的发展,JavaScript框架不断进化,...了解和掌握单例模式不仅可以帮助我们在日常开发中编写出更好的代码,还可以让我们更加深刻地理解JavaScript的核心机制。

    PHP最常用的2种设计模式工厂模式和单例模式介绍

    在实际应用中,数据库连接通常使用单例模式,因为数据库连接的建立和销毁都需要耗费一定资源,如果每次请求都创建新的连接,将导致系统性能下降。使用单例模式可以保证整个应用只有一个数据库连接实例,节约资源,...

    js代码-设计模式之单例模式2

    在JavaScript中,由于其全局作用域和函数级别作用域,如果不加以控制,很容易产生多个相同对象的实例。 **JavaScript中的单例实现** 在JavaScript中,实现单例模式有以下几种常见方法: 1. **闭包**: ```...

    java设计模式

    在这个主题中,我们将深入探讨三个主要的设计模式:单例模式、工厂模式和装饰模式。 **单例模式** 是一种限制类实例化的模式,确保一个类在整个程序运行过程中只有一个实例存在。它通过控制类的构造函数来实现这一...

    Python掌握设计模式.pdf

    创建型模式涉及对象的创建过程,包括单例模式、工厂模式、建造者模式等。结构型模式关注对象与类的组合,例如适配器模式、组合模式和装饰器模式。行为型模式则关注对象间的通信,比如观察者模式、命令模式和策略模式...

    Java设计模式面试题汇总

    单例模式:单例模式属于创建型模式,一个单例类在任何情况下都只存在一个实例,构造方法必须是私有的、由自己创建一个静态变量存储实例,对外提供一个静态公有方法获取实例。 这些设计模式都是软件开发人员需要了解...

    尚硅谷设计模式源码笔记课件.zip

    2) 设计模式包含了大量的编程思想,讲授和真正掌握并不容易,网上的设计模式课程不少,大多讲解的比较晦涩,没有真实的应用场景和框架源码支撑,学习后,只知其形,不知其神。就会造成这样结果: 知道各种设计模式,...

    java设计模式ppt

    学习和掌握这些设计模式不仅能够提升代码质量,还有助于团队之间的沟通,因为它们提供了一种共同的语言和理解,使得复杂问题的解决方案更容易被理解和复用。在阅读这个“Java设计模式”PPT时,建议结合实际案例来...

    设计模式(包含5个设计模式)含源代码报告.zip

    在给定的压缩包文件中,包含了五个重要的设计模式,它们分别是单例模式、工厂方法模式、观察者模式、外观模式和代理模式。每个模式都有其独特的应用场景和优势,下面将对这些模式进行详细讲解。 1. 单例模式...

    常用设计模式例题(原创)

    在给定的压缩包文件中,我们可以看到涉及到九种基本的设计模式,它们分别是:组合模式(Composite)、策略模式(Strategy)、外观模式(Facade)、观察者模式(Observer)以及单例模式(Singleton)。接下来,我们将...

    设计模式解析

    创建型模式关注对象的创建过程,如单例模式、工厂模式和建造者模式等,它们帮助我们控制实例化过程,避免过早或过晚的对象创建。结构型模式则关注如何组合类和对象,以实现更复杂的设计,如适配器模式、装饰器模式和...

    设计模式可复用面向对象软件的基础.

    2. 单例模式:单例模式也是一种创建型设计模式,它保证一个类只有一个实例,并提供一个全局访问点。这种模式常用于控制资源的访问,如数据库连接池、线程池或者配置文件读取等。在C++中实现单例,通常会使用静态成员...

    java设计模式 课件讲义

    创建型模式包括单例模式、工厂模式、抽象工厂模式、建造者模式和原型模式,它们主要关注对象的创建过程。结构型模式包括代理模式、装饰器模式、适配器模式、桥接模式、组合模式、外观模式和享元模式,它们关注如何...

    设计模式可复用面向对象软件的基础(C++)——强烈推荐

    创建型模式关注对象的创建,如单例模式、工厂模式和建造者模式,它们旨在提供一种灵活的、抽象的对象创建方式。结构型模式关注如何将对象组合成更大的结构,例如适配器模式、装饰器模式和代理模式。行为型模式则涉及...

    Java之23种设计模式解析

    1. **创建型模式**(Creational Patterns):这类模式主要关注对象的创建过程,包括单例模式、工厂方法模式、抽象工厂模式、建造者模式和原型模式。它们提供了一种在不指定具体类的情况下创建对象的方式,提高了代码...

    C#面向对象设计模式纵横谈(1):面向对象设计模式与原则

    - 创建型模式:单例模式、工厂方法模式、抽象工厂模式、建造者模式和原型模式。这些模式关注于对象的创建,使得对象的创建过程更加灵活和可控。 - 结构型模式:适配器模式、桥接模式、装饰器模式、外观模式、组合...

Global site tag (gtag.js) - Google Analytics