设计模式:单例模式(精讲)

设计理念

在有些系统中,为了节省内存资源、保证数据内容的一致性,对某些类要求只能创建一个实例,这就是所谓的单例模式。
单例模式,是23种设计模式中使用最广泛的一种设计模式,同是也是最重要的设计模式之一。

定义

单例(Singleton)模式的定义:指一个类只有一个实例,且该类能自行创建这个实例的一种模式。例如,Windows 中只能打开一个任务管理器,这样可以避免因打开多个任务管理器窗口而造成内存资源的浪费,或出现各个窗口显示内容的不一致等错误。

在计算机系统中,还有 Windows 的回收站、操作系统中的文件系统、多线程中的线程池、显卡的驱动程序对象、打印机的后台处理服务、应用程序的日志对象、数据库的连接池、网站的计数器、Web 应用的配置对象、应用程序中的对话框、系统中的缓存等常常被设计成单例。

特点

单例模式有 3 个特点:

  1. 单例类只有一个实例对象;
  2. 该单例对象必须由单例类自行创建;
  3. 单例类对外提供一个访问该单例的全局访问点;

结构与实现

单例模式是设计模式中最简单的模式之一。通常,普通类的构造函数是公有的,外部类可以通过“new 构造函数()”来生成多个实例。但是,如果将类的构造函数设为私有的,外部类就无法调用该构造函数,也就无法生成多个实例。这时该类自身必须定义一个静态私有实例,并向外提供一个静态的公有函数用于创建或获取该静态私有实例。

结构

单例模式的主要角色如下。

  1. 单例类:包含一个实例且能自行创建这个实例的类。
  2. 访问类:使用单例的类。

其结构如图 1 所示。

在这里插入图片描述

实现

单例模式最常见的有6种实现方式,每种方式都有其特点,都值得大家去仔细推敲。

  1. 懒汉式,线程不安全

    代码实例:

package com.design.pattern.creationalPattern.singletonPattern;

public class Singleton {

    //Q:这里可不可以去掉static?
    //A:不可以,因为getInstance()方法是静态方法,
    //不可以在静态方法里调用非静态变量
    private static Singleton instance;

    private Singleton(){
        //构造器私有化,防止外部访问,通过new Singleton()方式创建对象
    }

    /**
     * Q:如果多个线程同时访问,会怎样?
     * A:严格意义上,这种方式不能算真正意义上的单例。
     *   当线程并发,线程上下文切换时,
     *   假设 有线程一和线程二。
     *   线程一 通过 if(instance == null )后,
     *   在 nstance = new Singleton(); 被挂起
     *   线程二 正常执行完了整个getInstance()后,
     *   线程一获得线程使用权。
     *   这时候线程一拿到的instance实例,就不是之前的实例对象了。
     * */
    public static Singleton getInstance(){

        if (instance == null){
            instance = new Singleton();
        }
        return instance;
    }


}

对于它线程不安全的情况,我们做个试验来验证。

package com.design.pattern.creationalPattern.singletonPattern;


public class UnSafeSingletonTest {

    public static void main(String[] args) {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                Singleton instance1 = Singleton.getInstance();
                System.out.println(instance1);

            }
        });
        thread1.start();
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                Singleton instance2 = Singleton.getInstance();//在这里打上debug断点,当断点到达这个位置时,在Singleton#getInstance()方法中instance = new Singleton(),打上断点
                System.out.println(instance2);

            }
        });
        thread2.start();


    }
}

模拟线程上下文切换,这里我模拟休眠线程,将断点位置先打到了Singleton 类的instance = new Singleton()位置。

  1. 当线程一过来的时候,直接放开断点,可以看到线程1和线程2同时到达,如图一。
    在这里插入图片描述
  2. 当线程二过来的时候,在debug watches里让线程二休眠2毫秒,模拟线程上下文切换场景。如图二
    在这里插入图片描述
  3. 再放开断点,可以明显的看到线程一和线程二都去创建了实例对象,并且线程一现在持有的引用是线程二创建的对象地址。这时候,线程一再去使用instance就可能出现问题,得到结果如下:

SingletonThread2 SingletonThread1
com.design.pattern.creationalPattern.singletonPattern.Singleton@17ceefc4
com.design.pattern.creationalPattern.singletonPattern.Singleton@1d4331b

  1. 懒汉式,线程安全
package com.design.pattern.creationalPattern.singletonPattern;

public class Singleton {

    //Q:这里可不可以去掉static?
    //A:不可以,因为getInstance()方法是静态方法,不可以在静态方法里调用非静态变量
    private static Singleton instance;

    private Singleton(){
        //构造器私有化,防止外部访问,通过new Singleton()方式创建对象
    }

    /**
     * synchronized 关键字可以保证方法或者代码块在运行时,同一时刻只有一        个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性
     * */
    public synchronized static Singleton getInstance(){

        if (instance == null){
            instance = new Singleton();
        }
        return instance;
    }


}

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

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

缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。

synchronized 关键字JDK随着版本升级一直在进行优化,其实效率已经很不错了,但是编程是一门科学性学科,实事求是的说,加这个关键字,会影响效率。

  1. 饿汉式
public class HungrySingleton {

    //private 私有化,外部不得调用,static 启动时就分配该对象内存
    private static HungrySingleton instance = new HungrySingleton();

    private HungrySingleton(){} //私有化构造器,外部无法调用

    //提供类静态方法,直接调用,得到唯一实例。
    public static HungrySingleton getInstance(){
        return instance;
    }

}

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

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

缺点:类加载时就初始化,浪费内存。

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

浪费内存的原因,这个类在你的项目启动时,就会去加载到JVM里,如果你没有去使用,就占据了一小块内存,如果项目中存在很多单例对象,会导致资源的浪费。

  1. 双检锁/双重校验锁(DCL,即 double-checked locking)
package com.design.pattern.creationalPattern.singletonPattern;

public class Singleton {

    //这里加入volatile使其在多线程操作时,保持可见性
    private volatile static Singleton instance;

    private Singleton(){
        //构造器私有化,防止外部访问,通过new Singleton()方式创建对象
    }
    
    public  static Singleton getInstance(){

        if (instance == null){ //第一次检查instance是不是null 
            synchronized (Singleton.class){ //第二次加锁再去检查一遍是不是空,防止多线程并发导致线程上下文切换,生成多个实例对象。
                if (instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }


}

这种方式采用双锁机制,也是懒加载方式实现,安全且在多线程情况下能保持高性能。
getInstance() 的性能对应用程序很关键。

  1. 登记式/静态内部类
package com.design.pattern.creationalPattern.singletonPattern;

public class Singleton {


    private Singleton(){
        //构造器私有化,防止外部访问,通过new Singleton()方式创建对象
    }

    public  static Singleton getInstance(){

        return SingletonHolder.instance;
    }

    private static class SingletonHolder{
        
        private static final Singleton instance = new Singleton();
    }


}

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

  1. 枚举
public enum Singleton {  
    INSTANCE;  
    public void whateverMethod() {  
    }  
}

这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,因为枚举类型是线程安全的,并且只会装载一次,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值