设计模式之单例模式

详细内容请移步至语雀文档:点击跳转

一、单例模式概述

1.1 什么是单例模式

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一,指一个类只有一个实例,且该类能自行创建这个实例的一种模式例如J2EE 标准中的 ServletContext 和 ServletContextConfig、Spring 框架应用中的 ApplicationContext、数据库中的连接池等也都是单例模式。
       这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建,即保证一个类仅有一个实例。这个类提供了一种访问其唯一对象的全局访问点,可直接访问,不需要实例化该类的对象。
       单例模式解决了一个全局使用的类频繁的创建与销毁。单例模式提供一个获取自己实例对象的方法,其构造方法和成员变量均为私有。
       单例模式分三种:懒汉式单例、饿汉式单例、登记式单例三种。

1.2 单例模式的特点

  • 单例类只能有一个实例。
  • 单例类必须自己创建自己的唯一实例。
  • 单例类必须给所有其他对象提供这一实例。

       单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。

1.3 单例模式的优缺点

1.3.1 单例模式的优点:

  • 单例模式可以保证内存里只有一个实例,减少了内存的开销。
  • 可以避免对资源的多重占用。
  • 单例模式设置全局访问点,可以优化和共享资源的访问。

1.3.2 单例模式的缺点:

  • 单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则。
  • 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
  • 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。

1.4  单例的应用场景

对于Java来说,单例模式可以保证在一个 JVM 中只存在单一实例。单例模式的应用场景主要有以下几个方面。

  • 需要频繁创建的一些类,使用单例可以降低系统的内存压力,减少 GC。
  • 某类只要求生成一个对象的时候,如一个班中的班长、每个人的身份证号等。
  • 某些类创建实例时占用资源较多,或实例化耗时较长,且经常使用。
  • 某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池、网络连接池等。
  • 频繁访问数据库或文件的对象。
  • 对于一些控制硬件级别的操作,或者从系统上来讲应当是单一控制逻辑的操作,如果有多个实例,则系统会完全乱套。
  • 当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如 Web 中的配置对象、数据库的连接池等。

二、单例模式示例

①创建一个单例类

public class SingleObject {
    //在该单例类中自己创建自己的实例对象
    private static SingleObject instance = new SingleObject();

    //构造函数为private修饰,该类不会被实例化
    private SingleObject() {
    }

    //获取唯一的可用对象
    public static SingleObject getInstance(){
        return instance;
    }

    public void showMessage(){
        System.out.println("Hello World");
    }
}

②测试

public class SingletonPatternDemo {
    public static void main(String[] args) {

        //直接用构造函数创建对象时,编译出错,因为构造函数singleObject不可见
        //SingleObject singleObject = new SingleObject();

        //获取为一可用的实例
        SingleObject instance = SingleObject.getInstance();
        instance.showMessage();
    }
}

三、单例模式的六种实现方式

单例模式分三种:懒汉式单例、饿汉式单例、登记式单例三种。

3.1 懒汉式单例

3.1.1 懒汉式(线程不安全)

是否Lazy初始化(懒加载)
是否多线程安全
实现难度

描述:这种方式是最基本的实现方式,也是最简单的单例模式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。这种方式lazy loading(懒加载)很明显,不要求线程安全,在多线程不能正常工作。在该实现方式中,构造方法是私有的,别人不能访问你的实例,提供一个static方法以供他人获取你的实例对象。

public class Singleton {  
    private static Singleton instance;
    //构造方法私有
    private Singleton (){}  
  
    //供他人获取实例对象的静态方法
    public static Singleton getInstance() {  
    if (instance == null) {  
        instance = new Singleton();  
    }  
    return instance;  
    }  
}

3.1.2 懒汉式(线程安全)

是否Lazy初始化(懒加载)
是否多线程安全

实现难度

描述:这种方式具备很好的懒加载,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。

优点:第一次调用才初始化,避免内存浪费。

缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。

在第一种懒汉式中加入了一个关键字synchronized, 使用synchronized保证线程同步,保证同时只有一个进程进入此方法。从而保证并发安全。getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。

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

3.2 饿汉式

是否Lazy初始化(懒加载)
是否多线程安全
实现难度

描述:这种方式比较常用,但容易产生垃圾对象。

优点:没有加锁,执行效率会提高。

缺点:类加载时该单例对象就被初始化,浪费内存。

它基于 classloader 机制避免了多线程的同步问题,不过,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance方法,但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达懒加载的效果。

//静态常量
public class Singleton {  
    //类加载时就获得实例对象
    private static final Singleton instance = new Singleton();  
    private Singleton (){}  
    public static Singleton getInstance() {  
        return instance;  
    }  
}

//静态代码块
public class Singleton {  
    //类加载时就获得实例对象
    private static final Singleton instance ;
    static{
        instance = new Singleton();  
    }
    private Singleton (){}  
    public static Singleton getInstance() {  
        return instance;  
    }  
}

 

3.3 双检锁/双重校验锁(DCL,即 double-checked locking)

是否Lazy初始化(懒加载)

是否多线程安全
实现难度

复杂

描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。getInstance() 的性能对应用程序很关键。方法声明上去除了synchronized关键字,多线程进入方法内部,判断是否为null,如果为null,多个线程同时进入if块内,此时,我们是用Singleton Class对象同步一段方法。保证只有一个线程进入该方法。并且判断是否为null,如果为null,就进行初始化。我们想象一下,如果第一个线程进入进入同步块,发现该实例为null,于是进入if块实例化,第二个线程进入同步内则发现实例已经不是null,直接就返回了,从而保证了并发安全。那么这个和第二种方式又什么区别呢?第二种方式的缺陷是:每个线程每次进入该方法都需要被同步,成本巨大。而第四种方式呢?每个线程最多只有在第一次的时候才会进入同步块,也就是说,只要实例被初始化了,那么之后进入该方法的线程就不必进入同步块了。就解决并发下线程安全和性能的平衡。虽然第一次还是会被阻塞。但相比较于第二种,已经好多了。

修饰变量的volatile关键字,为什么要用volatile关键字呢?这是个有趣的问题。我们好好分析一下:首先我们看,Java虚拟机初始化一个对象都干了些什么?总的来说,3件事情:

1.在堆空间分配内存

2.执行构造方法进行初始化

3.将对象指向内存中分配的内存空间,也就是地址

但是由于当我们编译的时候,编译器在生成汇编代码的时候会对流程进行优化,优化的结果式有可能是123顺序执行,也有可能式132执行,但是,如果是按照132的顺序执行,走到第三步(还没到第二步)的时候,这时突然另一个线程来访问,走到if(singleton == null)块,会发现singleton已经不是null了,就直接返回了,但是此时对象还没有完成初始化,如果另一个线程对实例的某些需要初始化的参数进行操作,就有可能报错。使用volatile关键字,能够告诉编译器不要对代码进行重排序的优化。就不会出现这种问题了。

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
        //多线程直接访问,不做控制,不影响性能
        if (singleton == null) {  
            //如果有多个线程进入,则进入同步块,其余线程等待
            synchronized (Singleton.class) {  
                //此时第一个线程判断为空,第二个进来时就不为空
                if (singleton == null) {  
                    //第一个线程实例化此对象
                    singleton = new Singleton();  
                }  
            }  
        }  
    return singleton;  
    }  
}

3.4 登记式/静态内部类

是否Lazy初始化(懒加载)

是否多线程安全
实现难度

一般

描述:这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。

这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟第 3 种方式不同的是:第 3 种方式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 3 种方式就显得很合理。

public class Singleton {  
    private static class SingletonHolder {  
        private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
        return SingletonHolder.INSTANCE;  
    }  
}

3.5 反射和反序列化破坏单例

image

   我们知道Java的反射几乎是什么事情都能做,管你什么私有的公有的。都能破坏。我们是没有还手之力的。精心编写的代码就被破坏了,而反序列化也很厉害,但是稍微还有点办法遏制。什么办法呢?重写readResolve方法。

  我们看到:我们重写了readResolve方法,在该方法中直接返回了我们的内部类实例。重写readResolve() 方法,防止反序列化破坏单例机制,这是因为:反序列化的机制在反序列化的时候,会判断如果实现了serializable或者externalizable接口的类中包含readResolve方法的话,会直接调用readResolve方法来获取实例。这样我们就制止了反序列化破坏我们的单例模式。

3.6 枚举

是否Lazy初始化(懒加载)

是否多线程安全
实现难度

描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。

不能通过 reflection attack 来调用私有构造方法。

public enum Singleton {  
    INSTANCE;  
    public void whateverMethod() {  
    }  
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值