博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
单例模式
阅读量:6841 次
发布时间:2019-06-26

本文共 7725 字,大约阅读时间需要 25 分钟。

  hot3.png

1. 模式介绍

模式的定义

确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

模式的使用场景

确保某个类有且只有一个对象的场景,例如创建一个对象需要消耗的资源过多,如要访问 IO 和数据库等资源。

在Java应用中,单例对象能保证在一个JVM中,该对象只有一个实例存在。这样的模式有几个好处:

1、某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销。

2、省去了new操作符,降低了系统内存的使用频率,减轻GC压力。

3、有些类如交易所的核心交易引擎,控制着交易流程,如果该类可以创建多个的话,系统完全乱了。(比如一个军队出现了多个司令员同时指挥,肯定会乱成一团),所以只有使用单例模式,才能保证核心交易服务器独立控制整个流程。

2. UML类图

url

角色介绍

  • Client : 高层客户端。
  • Singleton : 单例类。

3. 模式的实现

简单实现的介绍

单例模式是设计模式中最简单的,只有一个单例类,没有其他的层次结构与抽象。该模式需要确保该类只能生成一个对象,通常是该类需要消耗太多的资源或者没有没有多个实例的理由。

首先我们写一个简单的单例类:

1.饿汉模式

public class Singleton {    /* 持有私有静态实例,防止被引用,同时实例在类加载的时候就创建出来 */            private static Singleton final instance = new Singleton();    /* 私有构造方法,防止被实例化 */    private Singleton() { }    /* 静态工程方法,创建实例 */    public static Singleton getInstance() {        return instance;    }}/*在需要处理构造异常的情况下*/public class Singleton {          private static Singleton final instance;    static {          try {             instance = new Singleton();          } catch (Exception e) {             throw new RuntimeException("an error's occurred!", e);          }      }     private Singleton() { }    public static Singleton getInstance() {        return instance;    }}

这个方法实现的单例是线程安全的。但是这个方法却牺牲了Lazy(懒加载)的特性。单例类加载的时候就实例化了。非懒加载,如果构造的单例很大,构造完又迟迟不使用,会导致资源浪费。

2.懒汉模式

public class Singleton {    /* 持有私有静态实例,防止被引用,此处赋值为null,目的是实现延迟加载 */    private static Singleton instance = null;    private Singleton() { }    public static Singleton getInstance() {        if (instance == null) {            instance = new Singleton();        }        return instance;    }}

这个改造我们将单例对象的创建放到了真正获取单例对象的方法中,在用户第一次调用getInstance时才进行初始化,但是,像这样毫无线程安全保护的类,如果我们把它放入多线程的环境下,肯定就会出现问题了

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

这次改造我们在getInstance方法加synchronized关键字,也就是getInstance是一个同步方法,这就是上面所说的在多线程情况下保证单例对象唯一性的手段.但是上面方法的问题在于即使instance已经被初始化了,但是每次调用getInstance方法都会进行同步,消耗不必要的资源(synchronized的静态方法在同步时会锁住整个Singleton类,导致其他调用该类的静态同步方法的线程都将被阻塞).这在并发要求较高的web应用中是不可取.我发现公司核电项目的service类都是这么去实现单例,当然在高版本的jdk中对synchronized进行了优化,而且公司项目对并发性没有任何要求,这样的写法也未尝不可以.

3.Double CheckLock(DCL双重检查锁)模式

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

DCL方式的优点是既能在需要的时候才初始化单例,又能够保证线程安全,且单例对象初始化后调用getInstance不进行同步锁.接下来看instance = new Singleton()语句,这里看起来是一句代码,但是实际上它并不是一个原子操作.这句代码最终会被编译成多条汇编指令.它大致做了3步:

(1)给Singleton的实例分配内存;

(2)调用Singleton()的构造函数,初始化成员字段;

(3)将instance对象指向分配的内存空间(此时instance就不为null了).

    但是,由于Java编译器允许处理器乱序执行,以及JDK1.5之前JMM(Java Memory Model,java内存模型)中Cache,寄存器到主存回写顺序的规定.上面(2)(3)的顺序是无法保证的.也就是说有可能JVM会先为新的Singleton实例分配空间,然后直接赋值给instance,然后再去初始化这个Singleton实例.此时instance就已经不为null了,但是该对象其实还没有进行初始化,这时候如果cpu切换到另一个线程.该线程获取到的就是还没有初始化的instance,这时该线程使用instance时就会出错.这就是DCL失效问题.而且这种难以跟踪难以重现的错误很可能会隐藏很久.

    在JDK1.5之后,SUN官方调整了JMM,具体化了volatile关键字,因此JDK1.5之后的版本可以使用下面的代码

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

volatile关键字保证了instance对象每次都是从主存中读取,而不是线程内存的副本对象,当然volatile多少还是会影响性能,也由于Java内存模型的原因偶尔也会失败.特别是在高并发的环境下.虽然发生概率很小.DCL模式是使用最多的单例实现方式,它能够在需要的时候才初始化单例对象,并且能够在绝大多数场景下保证单例对象的唯一性.除非你的代码在并发场景比较复杂,或者低于JDK6版本下使用.否则,这种方式一般能够满足需求.

对于DCL失效问题,也有人这样实现:因为我们只需要在创建类的时候进行同步,所以只要将创建和getInstance()分开,单独为创建加synchronized关键字,也是可以的:

public class Singleton {    private static Singleton instance = null;    private Singleton() { }    private static synchronized void syncInit() {        if (instance == null) {            instance = new Singleton();        }    }    public static Singleton getInstance() {        if (instance == null) {            syncInit();        }        return instance;    }}

4.静态内部类单例模式

    DCL虽然在一定程度上解决了资源消耗,多余的同步,线程安全等问题.在<<Java并发编程实践>>一书中最后谈到了这个双重检查锁定失效的问题.并指出这种优化是丑陋的,不赞成使用,而建议使用下面的代码

public class Singleton {    private Singleton() { }    public static Singleton getInstance () {        return SingletonHolder.instance;    }    /*私有静态内部类*/    private static class SingletonHolder {        private static final Singleton instance = new Singleton();    }}

    单例模式使用内部类来维护单例的实现,JVM内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。这样当我们第一次调用 getInstance的时候,JVM能够帮我们保证instance只被创建一次,并且会保证把赋值给instance的内存初始化完毕,这样我们就不 用担心上面的问题。同时该方法也只会在第一次调用的时候使用互斥机制,这样就解决了低性能问题。

    当第一次加载Singleton类时并不会初始化instance,只有在第一次调用Singleton的getInstance方法时才会导致instance被初始化.因此,第一次调用getInstance方法会导致虚拟机加载SingletonHolder类,这种方式不仅能够确保线程安全.也能够保证单例对象的唯一性,同时也延迟了单例的实例化,所以这是推荐使用的单例模式实现方式.

5.枚举单例

public enum SingletonEnum {    INSTANCE;    public void doSomething() {        System.out.println("do sth.");    }}

写法简单是枚举单例最大的优点,枚举在Java中与普通的类是一样的,不仅能够有字段,还能够有自己的方法.最重要的是默认枚举实例的创建是线程安全的,并且在任何情况下它都是一个单例.

    为什么这么说呢?在上述的几个单例模式实现中,在一个情况下它们会出现重新创建对象的情况,那就是反序列化.

通过序列化可以将一个单例的实例对象写到磁盘,然后再读回来,从而有效地获得一个实例.即使构造函数是私有的,反序列化时依然可以通过特殊的途径去创建类的一个新的实例,相当于调用该类的构造函数.反序列化操作提供了一个很特别的钩子函数,类中具有一个私有的,被实例化的readResolve(),这个方法可以让开发人员控制对象的反序列化.上面的例子中如果要杜绝单例对象在被反序列化时重新生成对象.那么就必须重写Object的如下方法:

private Object readResolve() throws ObjectStreamException {    return instance;}

也就是在readResolve方法中将instance对象返回,而不是默认的重新生成一个新的对象.而对于枚举,并不存在这个问题,因为即使反序列化它也不会重新生成新的实例.

6.使用容器实现单例模式

public class SingletonManager {    private static Map
objMap = new HashMap
(); private Singleton() { } public static void registerService(String key, Objectinstance) { if (!objMap.containsKey(key) ) { objMap.put(key, instance) ; } } public static ObjectgetService(String key) { return objMap.get(key) ; }}

在程序的初始,将多个单例类型注入到一个统一的管理类中,在使用时根据key获取对象对应类型的对象.这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度.

7.影子实例(为单例对象的属性同步更新)    

public class Singleton {        private static Singleton instance = null;        private Vector properties = null;        public Vector getProperties() {            return properties;        }        private Singleton() { }        private static synchronized void syncInit() {            if (instance == null) {                instance = new Singleton();            }        }        public static Singleton getInstance() {            if (instance == null) {                syncInit();            }            return instance;        }        public void updateProperties() {            Singletonshadow = new Singleton();            properties = shadow.getProperties();        }    }

    首先,静态类不能实现接口。(从类的角度说是可以的,但是那样就破坏了静态了。因为接口中不允许有static修饰的方法,所以即使实现了也是非静态的)

    其次,静态类内部方法都是static,无法被覆写。

    最后一点,单例类比较灵活,毕竟从实现上只是一个普通的Java类,只要满足单例的基本需求,你可以在里面随心所欲的实现一些其它功能,但是静态类不行。静态内部类模式内部就是用一个静态类来实现的,就像HashMap采用数组+链表来实现一样,其实生活中很多事情都是 这样,单用不同的方法来处理问题,总是有优点也有缺点,最完美的方法是,结合各个方法的优点,才能最好的解决问题!

    不管以哪种形式实现单例模式,他们的核心原理都是将构造函数私有化,并且通过静态方法获取一个唯一的实例,在这个获取的过程中必须保证线程安全,防止反序列化导致重新生成实例对象等问题.选择哪种实现方式取决于项目本身,比如是否复杂的并发环境,JDK版本是否过低,单例对象的资源消耗等.

4. 杂谈

优点与缺点

优点

  • 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。
  • 由于单例模式只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后用永久驻留内存的方式来解决;
  • 单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。
  • 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。

缺点

  • 单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。

转载于:https://my.oschina.net/fuyong/blog/717679

你可能感兴趣的文章
【批处理学习笔记】第十九课:字符串合并
查看>>
混合两张图
查看>>
Cognos请求流程——<转>
查看>>
黄聪:wordpress用httpd.ini伪静态不支持中文解决办法
查看>>
HDU 1527 取石子游戏
查看>>
hdu 3944 dp?
查看>>
第四天个人总结
查看>>
JPA基础(三)(转)
查看>>
省市联动案例
查看>>
LeetCode174 Dungeon Game
查看>>
leetcode 戳气球
查看>>
python基础:datetime模块-毫秒时间差-微秒时间差
查看>>
一道闭包题引发的思考
查看>>
给一个div元素添加多个背景图片
查看>>
6.Git工具
查看>>
JAVA入门到精通-第19讲-多维数组
查看>>
RESTful 架构详解
查看>>
mvc.net分页查询案例——DLL数据访问层(HouseDLL.cs)
查看>>
多重映射
查看>>
Ubuntu建立(apache+php+mysql)+phpmyadmin
查看>>