探索设计模式之----单例模式

Singleton模式可以是很简单的,它的全部只需要一个类就可以完成。但是如果在“对象创建的次数以及何时被创建”这两点上较真起来,Singleton模式可以相当的复杂。

结构是简单的,只是我们还有一些小小的要求如下:

1.最基本要求:每次从getInstance()都能返回一个且唯一的一个对象。

2.稍微高一点的要求:希望这个方法能适应多线程并发访问。

3.再提高一点的要求:方法性能尽可能高。

4.最后一点要求是:希望实现懒加载(Lazy Load),在需要的时候才被构造。

目的:希望对象只创建一个实例,并且提供一个全局的访问点。

我们第一次写的单例模式是下面这个样子的:

public class SingletonKerriganA {  

    /**
     * 单例对象实例
     */
    private static SingletonKerriganA instance = null;  

    public static SingletonKerriganA getInstance() {
        if (instance == null) {                              //line A
            instance = new SingletonKerriganA();          //line B
        }
        return instance;
    }
}  

这个写法我们把四点需求从上往下检测,发现第二点的时候就出了问题,假设这样的场景:两个线程并发调用SingletonKerriganA.getInstance(),假设线程一先判断完instance是否为null,既代码中的line A进入到line B的位置。刚刚判断完毕后,JVM将CPU资源切换给线程二,由于线程一还没执行line B,所以instance仍然是空的,因此线程二执行了new SignletonKerriganA()操作。片刻之后,线程一被重新唤醒,它执行的仍然是new SignletonKerriganA()操作,好了,问题来了,两个Kerrigan谁是李逵谁是李鬼?

紧接着,我们做单例模式的第二次尝试:

public class SingletonKerriganB {  

    /**
     * 单例对象实例
     */
    private static SingletonKerriganB instance = null;  

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

比起第一段代码仅仅在方法中多了一个synchronized修饰符,现在可以保证不会出线程问题了。但是这里有个很大(至少耗时比例上很大)的性能问题。除了第一次调用时是执行了SingletonKerriganB的构造函数之外,以后的每一次调用都是直接返回instance对象。返回对象这个操作耗时是很小的,绝大部分的耗时都用在synchronized修饰符的同步准备上,因此从性能上说很不划算。

那继续把代码改成下面的样子:

public class SingletonKerriganC {  

    /**
     * 单例对象实例
     */
    private static SingletonKerriganC instance = null;  

    public static SingletonKerriganC getInstance() {
        synchronized (SingletonKerriganC.class) {
            if (instance == null) {
                instance = new SingletonKerriganC();
            }
        }
        return instance;
    }
}  

基本上,把synchronized移动到代码内部是没有什么意义的,每次调用getInstance()还是要进行同步。同步本身没有问题,但是我们只希望在第一次创建Kerrigan实例的时候进行同步,因此我们有了下面的写法——双重锁定检查(DCL)。

public class SingletonKerriganD {  

    /**
     * 单例对象实例
     */
    private static SingletonKerriganD instance = null;  

    public static SingletonKerriganD getInstance() {
        if (instance == null) {
            synchronized (SingletonKerriganD.class) {
                if (instance == null) {
                    instance = new SingletonKerriganD();
                }
            }
        }
        return instance;
    }
}  

看起来这样已经达到了我们的要求,除了第一次创建对象之外,其他的访问在第一个if中就返回了,因此不会走到同步块中。已经完美了吗?

我们来看看这个场景:假设线程一执行到instance = new SingletonKerriganD()这句,这里看起来是一句话,但实际上它并不是一个原子操作(原子操作的意思就是这条语句要么就被执行完,要么就没有被执行过,不能出现执行了一半这种情形)。事实上高级语言里面非原子操作有很多,我们只要看看这句话被编译后在JVM执行的对应汇编代码就发现,这句话被编译成8条汇编指令,大致做了3件事情:

1.给Kerrigan的实例分配内存。

2.初始化Kerrigan的构造器

3.将instance对象指向分配的内存空间(注意到这步instance就非null了)

但是,由于Java编译器允许处理器乱序执行(out-of-order),以及JDK1.5之前JMM(Java Memory Medel)中Cache、寄存器到主内存回写顺序的规定,上面的第二点和第三点的顺序是无法保证的,也就是说,执行顺序可能是1-2-3也可能是1-3-2,如果是后者,并且在3执行完毕、2未执行之前,被切换到线程二上,这时候instance因为已经在线程一内执行过了第三点,instance已经是非空了,所以线程二直接拿走instance,然后使用,然后顺理成章地报错,而且这种难以跟踪难以重现的错误估计调试上一星期都未必能找得出来,真是一茶几的杯具啊。

DCL的写法来实现单例是很多技术书、教科书(包括基于JDK1.4以前版本的书籍)上推荐的写法,实际上是不完全正确的。的确在一些语言(譬如C语言)上DCL是可行的,取决于是否能保证2、3步的顺序。在JDK1.5之后,官方已经注意到这种问题,因此调整了JMM、具体化了volatile关键字,因此如果JDK是1.5或之后的版本,只需要将instance的定义改成“private volatile static SingletonKerriganD instance = null;”就可以保证每次都去instance都从主内存读取,就可以使用DCL的写法来完成单例模式。当然volatile或多或少也会影响到性能,最重要的是我们还要考虑JDK1.42以及之前的版本,所以本文中单例模式写法的改进还在继续。

一个类在一个ClassLoader中只会被初始化一次,这点是JVM本身保证的,那就把初始化实例的事情扔给JVM好了,代码被改成这样:

public class SingletonKerriganE {  

    /**
     * 单例对象实例
     */
    private static SingletonKerriganE instance = new SingletonKerriganE();  

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

好吧,如果这种写法是完美的话,那前面那么几大段话就是作者在消遣各位读者。这种写法不会出现并发问题,但是它是饿汉式的,在ClassLoader加载类后Kerrigan的实例就会第一时间被创建,饿汉式的创建方式在一些场景中将无法使用:譬如Kerrigan实例的创建是依赖参数或者配置文件的,在getInstance()之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。

再来看看下面这种我觉得能应对较多场景的单例写法:

public class SingletonKerriganF {  

    private static class SingletonHolder {
        /**
         * 单例对象实例
         */
        static final SingletonKerriganF INSTANCE = new SingletonKerriganF();
    }  

    public static SingletonKerriganF getInstance() {
        return SingletonHolder.INSTANCE;
    }
}  

这种写法仍然使用JVM本身机制保证了线程安全问题;由于SingletonHolder是私有的,除了getInstance()之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖JDK版本。

通过一次次的的尝试,去了解单例模式各种实现方案的优缺点。对双锁锁定检测进行了简单的讨论,相信大家能从各种尝试的演化过程中,明白为何单例模式是最简单而又最复杂的一种构造模式。

最后我们比较一下懒汉模式和饿汉模式的优缺点:

懒汉模式,它的特点是运行时获得对象的速度比较慢,但加载类的时候比较快。它在整个应用的生命周期只有一部分时间在占用资源。

饿汉模式,它的特点是加载类的时候比较慢,但运行时获得对象的速度比较快。它从加载到应用结束会一直占用资源。

这两种模式对于初始化较快,占用资源少的轻量级对象来说,没有多大的性能差异,选择懒汉式还是饿汉式都没有问题。但是对于初始化慢,占用资源多的重量级对象来说,就会有比较明显的差别了。所以,对重量级对象应用饿汉模式,类加载时速度慢,但运行时速度快;懒汉模式则与之相反,类加载时速度快,但运行时第一次获得对象的速度慢。

从用户体验的角度来说,我们应该首选饿汉模式。我们愿意等待某个程序花较长的时间初始化,却不喜欢在程序运行时等待太久,给人一种反应迟钝的感觉,所以对于有重量级对象参与的单例模式,我们推荐使用饿汉模式。

时间: 2024-08-29 06:31:09

探索设计模式之----单例模式的相关文章

探索设计模式目录

探索设计模式之一——简单工厂模式 http://icyfenix.iteye.com/blog/575040 探索设计模式之二——工厂方法模式 http://icyfenix.iteye.com/blog/575045 探索设计模式之三——抽象工厂模式 探索设计模式之四——建造者模式 探索设计模式之五——原型模式 探索设计模式之六——单例模式

Java设计模式:单例模式

概念: java中单例模式是一种常见的设计模式,单例模式的写法有好几种,这里主要介绍三种:懒汉式单例.饿汉式单例.登记式单例. 单例模式有以下特点: 1.单例类只能有一个实例. 2.单例类必须自己创建自己的唯一实例. 3.单例类必须给所有其他对象提供这一实例. 单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例.在计算机系统中,线程池.缓存.日志对象.对话框.打印机.显卡的驱动程序对象常被设计成单例.这些应用都或多或少具有资源管理器的功能.每台计算机可以有若干个打印机,但只能

[转]JAVA设计模式之单例模式

原文地址:http://blog.csdn.net/jason0539/article/details/23297037 概念: java中单例模式是一种常见的设计模式,单例模式的写法有好几种,这里主要介绍三种:懒汉式单例.饿汉式单例.登记式单例. 单例模式有以下特点: 1.单例类只能有一个实例. 2.单例类必须自己创建自己的唯一实例. 3.单例类必须给所有其他对象提供这一实例. 单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例.在计算机系统中,线程池.缓存.日志对象.对话

设计模式 之 单例模式

单例模式思路: 私有化构造方法: 防止实例化 私有化克隆方法: 防止克隆 私有化静态属性: 保存对象 公有化静态方法: 获取对象 代码: <?php //设计模式:单例模式 class Singleton { //私有化静态属性:用于保存对象 private static $obj; //私有化构造方法 private function __construct(){} //公有化静态方法:用于实例化对象 public static function getObj() { //判断对象是否存在 i

设计模式实例学习-单例模式(Android中的使用场景)

1.设计模式实例-单例模式 单例模式,故名思议,是指在一个类中通过设置静态使得其仅创造一个唯一的实例.这样设置的目的是满足开发者的希望--这个类只需要被实例化创建一次,同时因为其为静态的缘故,加载的速度也应该快于正常实例化一个类的速度(理论上). 在Android开发中,当我们需要创建一个Fragment的时候常常会用到这样的模式,没有代码的学习是虚无的,接下来亮代码学习: public class SelectFrame extends Fragment { private final sta

(九)JAVA设计模式之单例模式

JAVA设计模式之单例模式 一.单例模式的介绍 Singleton是一种创建型模式,指某个类采用Singleton模式,则在这个类被创建后,只可能产生一个实例供外部访问,并且提供一个全局的访问点.     全局对象和Singleton模式有本质的区别,因为大量使用全局对象会使得程序质量降低,而且有些编程语言根本不支持全局变量.最重要的是传统的全局对象并不能阻止一个类被实例化多次. 二.单例模式的特点 单例类只能有一个实例 单例类必须自己创建自己的唯一实例. 单例类必须给所有其他对象提供这一实例.

C#设计模式(1)——单例模式

一.引言 最近在设计模式的一些内容,主要的参考书籍是<Head First 设计模式>,同时在学习过程中也查看了很多博客园中关于设计模式的一些文章的,在这里记录下我的一些学习笔记,一是为了帮助我更深入地理解设计模式,二同时可以给一些初学设计模式的朋友一些参考.首先我介绍的是设计模式中比较简单的一个模式——单例模式(因为这里只牵涉到一个类) 二.单例模式的介绍 说到单例模式,大家第一反应应该就是——什么是单例模式?,从“单例”字面意思上理解为——一个类只有一个实例,所以单例模式也就是保证一个类只

.NET设计模式之(单例模式)

1.单例模式,一个类只能new一个对象 2.举例,资源管理器,文件管理器,地球等: 3.创建单例: (1)创建一个Earth类 class Earth { public Earth() { } } (2)将构造函数 私有化 class Earth { private Earth() { } } (3)声明一个静态私有的字段,初始化一个实例 class Earth { private static Earth instance=new Earth(); private Earth() { } }

Java 设计模式(3)单例模式

前言 概念: java中单例模式是一种常见的设计模式,单例模式的写法有好几种,这里主要介绍三种:懒汉式单例.饿汉式单例.登记式单例. 单例模式有以下特点: 1.单例类只能有一个实例. 2.单例类必须自己创建自己的唯一实例. 3.单例类必须给所有其他对象提供这一实例. 单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例.在计算机系统中,线程池.缓存.日志对象.对话框.打印机.显卡的驱动程序对象常被设计成单例.这些应用都或多或少具有资源管理器的功能.每台计算机可以有若干个打印机,