Java设计模式学习记录-单例模式

前言

已经介绍和学习了两个创建型模式了,今天来学习一下另一个非常常见的创建型模式,单例模式。

单例模式也被称为单件模式(或单体模式),主要作用是控制某个类型的实例数量是一个,而且只有一个。

单例模式

单例模式的实现方式

 实现单例模式的方式有很多种,大体上可以划分为如下两种。

外部方式

在使用某些全局对象时,做一些“try-Use”的工作。就是如果要使用的这个全局对象不存在,就自己创建一个,把它放到全局的位置上;如果本来就有,则直接拿来使用。

内部实现方式

类型自己控制正常实例的数量,无论客户程序是否尝试过了,类型自己自己控制只提供一个实例,客户程序使用的都是这个现成的唯一实例。

目前随着集群、多核技术的普遍应用,想通过简单的类型内部控制失效真正的Singleton越来越难,试图通过经典单例模式实现分布式环境下的“单例”并不现实。所以目前介绍的这个单例是有语义限制的。

单例模式的特点

虽然单例模式也属于创建型模式,但是它是有自己独特的特点的。

  • 单例类只有一个实例。
  • 单例类自行创建该实例,在该类内部创建自身的实例对象。
  • 向整个系统公开这个实例接口。

还有需要注意的一点,单例模式只关心类实例的创建问题,并不关心具体的业务功能。

单例模式的范围

目前Java里面实现的单例是一个ClassLoader及其子ClassLoader的范围。因为ClassLoader在装载饿汉式实现的单例类时,会响应地创建一个类的实例。这也说明,如果一个虚拟机里有多个ClassLoader(虽然说ClassLoader遵循双亲委派模型,但是也会有父加载器处理不了,然后自定义的加载器执行类加载的情况。),而且这些ClassLoader都装载着某一个类的话,就算这个类是单例,它也会产生很多个实例。如果一个机器上有多个虚拟机,那么每个虚拟机里面都应该至少有一个这个类的实例,也就是说整个机器上就有很多个实例,更不会是单例了。

还有一点再次强调,目前讨论的单例范围不适用于集群环境。

单例模式的类型

饿汉式单例

饿汉式单例是指在类被加载的时候,唯一实例已经被创建。

如下代码的例子:

/**
 * 饿汉式单例模式
 *
 */
public class HungrySingleton {
    
     * 定义一个静态变量用来存储实例,在类加载的时候创建,只会创建一次。
     */
    private static HungrySingleton hungrySingleton = new HungrySingleton();

    
     * 私有化构造方法,禁止外部创建实例。
     private HungrySingleton(){
        System.out.println("创建实例");
    }

    
     * 外部获取唯一实例的方法
     * @return
     static HungrySingleton getInstance(){
        return hungrySingleton;
    }

}

懒汉式单例

懒汉式单例是指在类加载的时候不创建单例的对象,只有在第一次使用的时候创建,并且在第一次创建后,以后不再创建该类的实例。

如下代码的例子:


 * 懒汉式单例
  LazySingleton {
    
     * 定义一个静态变量用来存储实例。
     static LazySingleton lazySingleton = null;

     LazySingleton(){}

    
     * 外部获取唯一实例的方法
* 当发现没有初始化的时候,才初始化静态变量。 *
LazySingleton getInstance(){ if(null==lazySingleton){ lazySingleton = LazySingleton(); } lazySingleton; } }

登记式单例

登记式单例实际上维护的是一组单例类的实例,将这些实例存在在一个登记薄(例如Map)中,使用已经登记过的实例,直接从登记簿上返回,没有登记的,则先登记,后返回。

如下代码例子:


 * 登记式单例
  RegisterSingleton {

    
     * 创建一个登记簿,用来存放所有单例对象
     static Map<String,RegisterSingleton> registerBook = new HashMap<>();

    
     * 私有化构造方法,禁止外部创建实例
      RegisterSingleton(){}

    
     * 注册实例
     * @param name 登记簿上的名字
     *  registerSingleton 登记簿上的实例
     static void registerInstance(String name,RegisterSingleton registerSingleton){

        if(!registerBook.containsKey(name)){
            registerBook.put(name,registerSingleton);
        }
    }

    
     * 获取实例,如果在未注册时调用将返回null
     *  RegisterSingleton getInstance(String name){
         registerBook.get(name);
    }
}

由于饿汉式的单例在类加载的时候就创建了一个实例,所以这个实例一直都不会变,因此也是线程安全的。但是懒汉式单例就不是线程安全的了,在懒汉式单例中有可能会出现两个线程创建了两个不同的实例,因为懒汉式单例中的getInstance()方法不是线程安全的。所以如果想让懒汉式变成线程安全的,需要在getInstance()方法中加锁。

如下所示:

   /**
     * 外部获取唯一实例的方法
     * 当发现没有被初始化的时候,才初始化静态变量
     * @return
     */
    synchronized lazySingleton;
    }

但是这样增加的资源消耗,延迟加载的效果虽然达到了,但是在使用的时候资源消耗确更大了,所以不建议这样用。既要实现线程安全,又要保证延迟加载。基于这样的问题就出现了另一种方式的单例模式,静态内部类式单例

静态内部类式单例

静态内部类式单例饿汉式和懒汉式的结合。

如下代码例子:


 * 内部静态类式单例
  StaticClassSingleton {

     StaticClassSingleton(){
        System.out.println("创建实例了"
     * 私有静态内部类,只能通过内部调用。
      SingleClass{
        static StaticClassSingleton singleton =  StaticClassSingleton();
    }

     StaticClassSingleton getInstance(){
         SingleClass.singleton;
    }

}

双重检查加锁式单例

上面静态内部类的方式通过结合饿汉式和懒汉式来实现了即延迟加载了又线程安全了。下面也来介绍另一种即实现了延迟加载有保证了线程安全的方式的单例。

如下代码例子:


 * 双重检查加锁式单例
  DoubleCheckLockSingleton {

    
     * 静态变量,用来存放实例。
     volatile static DoubleCheckLockSingleton doubleCheckLockSingleton =  DoubleCheckLockSingleton(){}

    
     * 双重检查加锁的方式保证线程安全又能获得到唯一实例
     *  DoubleCheckLockSingleton getInstance(){
        //先检查实例是否已经存在,不存在则进入代码块
        null == doubleCheckLockSingleton){
            synchronized (DoubleCheckLockSingleton.){
                由于synchronized也是重入锁,即一个线程有可能多次进入到此同步块中如果第一次进入时已经创建了实例,那么第二次进入时就不创建了。
                doubleCheckLockSingleton){
                    doubleCheckLockSingleton =  DoubleCheckLockSingleton();
                }
            }
        }

         doubleCheckLockSingleton;
    }

}

如上所示,所谓“双重检查加锁”机制,并不是每次进入getInstance()方法都需要加锁,而是当进入方法后,先检查实例是否已经存在,如果不存在才进行下面的同步块,这是第一重检查,进入同步块后,再次检查实例是否已经存在,如果不存在,就在同步块中创建一个实例,这是第二重检查。这个过程是只需要同步一次的。

还需要注意的一点是,在使用“双重检查加锁”时,需要在变量上使用关键字volatile,这个关键字的作用是,被volatile修饰的变量,所有对该变量的更新操作都是可以直接被其他线程可见的,从而确保多个线程能正确地处理该变量。可能不了解Java内存模式的朋友不太好理解这句话的意思,可以去看看(JVM学习记录-Java内存模型(一)JVM学习记录-Java内存模型(二))了解一下Java内存模型,我简单说明一下,volatile这个关键字可以保证每个线程操作的变量都会被其他线程所看到,就是说如果第一个线程已经创建了实例,但是把创建的这个实例只放在了自己的这个线程中,其他线程是看不到的,这个时候如果其他线程再去判断实例是否已经存在了实例的时候,发现没有还是没有实例就会又创建了一个实例,然后也放在了自己的线程中,如果这样的话我们写的单例模式就没意义了。在JDK1.5以前的版本中对volatile的支持存在问题,可能会导致“双重检查加锁”失败,所以如果要使用“双重检查加锁”式单例,只能使用JDK1.5以上的版本。

枚举式单例 

在JDK1.5中引入了一个新的特性,枚举,通过枚举来实现单例,在目前看来是最佳的方法了。Java的枚举类型实质上是功能齐全的类,因此可以有自己的属性和方法。

还是通过代码示例来解释吧。

如下代码例子:

/**
 * 单元素枚举实现单例模式
 */
enum EnumSingleton {

    
     * 必须是单元素,因为一个元素就是一个实例。
     */
    INSTANCE;

    
     * 测试方法1
     *  doSomeThing() {

        System.out.println("#####测试方法######"
     * 测试方法2
     * public String getSomeThing(){
        return "获得到了一些内容";
    }
}

 

上面例子中EnumSingleton.INSTANCE就可以获得到想要的实例了,调用单例的方法可以种EnumSingleotn.INSTANCE.doSomeThing()等方法。

下面来看看枚举是如何保证单例的:

首先枚举的构造方法明确是私有的,在使用枚举实例时会执行构造方法,同时每个枚举实例都是static final类型的,表明枚举实例只能被赋值一次,这样在类初始化的时候就会把实例创建出来,这也说明了枚举单例,其实是饿汉式单例方式。这样就用最简单的代码既保证了线程安全,又保证了代码的简洁。

还有一点很值得注意的是,枚举实现的单例保证了序列化后的单例安全。除了枚举式的单例,其他方式的单例,都可能会通过反射或反序列化来创建多个实例。

所以在使用单例的时候最好的办法就是用枚举的方式。既简洁又安全。

 

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


什么是设计模式一套被反复使用、多数人知晓的、经过分类编目的、代码 设计经验 的总结;使用设计模式是为了 可重用 代码、让代码 更容易 被他人理解、保证代码 可靠性;设计模式使代码编制  真正工程化;设计模式使软件工程的 基石脉络, 如同大厦的结构一样;并不直接用来完成代码的编写,而是 描述 在各种不同情况下,要怎么解决问题的一种方案;能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免引
单一职责原则定义(Single Responsibility Principle,SRP)一个对象应该只包含 单一的职责,并且该职责被完整地封装在一个类中。Every  Object should have  a single responsibility, and that responsibility should be entirely encapsulated by t
动态代理和CGLib代理分不清吗,看看这篇文章,写的非常好,强烈推荐。原文截图*************************************************************************************************************************原文文本************
适配器模式将一个类的接口转换成客户期望的另一个接口,使得原本接口不兼容的类可以相互合作。
策略模式定义了一系列算法族,并封装在类中,它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
设计模式讲的是如何编写可扩展、可维护、可读的高质量代码,它是针对软件开发中经常遇到的一些设计问题,总结出来的一套通用的解决方案。
模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中,使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
迭代器模式提供了一种方法,用于遍历集合对象中的元素,而又不暴露其内部的细节。
外观模式又叫门面模式,它提供了一个统一的(高层)接口,用来访问子系统中的一群接口,使得子系统更容易使用。
单例模式(Singleton Design Pattern)保证一个类只能有一个实例,并提供一个全局访问点。
组合模式可以将对象组合成树形结构来表示“整体-部分”的层次结构,使得客户可以用一致的方式处理个别对象和对象组合。
装饰者模式能够更灵活的,动态的给对象添加其它功能,而不需要修改任何现有的底层代码。
观察者模式(Observer Design Pattern)定义了对象之间的一对多依赖,当对象状态改变的时候,所有依赖者都会自动收到通知。
代理模式为对象提供一个代理,来控制对该对象的访问。代理模式在不改变原始类代码的情况下,通过引入代理类来给原始类附加功能。
工厂模式(Factory Design Pattern)可细分为三种,分别是简单工厂,工厂方法和抽象工厂,它们都是为了更好的创建对象。
状态模式允许对象在内部状态改变时,改变它的行为,对象看起来好像改变了它的类。
命令模式将请求封装为对象,能够支持请求的排队执行、记录日志、撤销等功能。
备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象。备忘录模式属于行为型模式。 基本介绍 **意图:**在不破坏封装性的前提下,捕获一个对象的内部状态,并在该
顾名思义,责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为
享元模式(Flyweight Pattern)(轻量级)(共享元素)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结