设计模式-单例模式介绍+8种实现方式

关于单例模式,思想很简单,就是确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,减少内存开支

主要有两大实现方式:懒汉式、饿汉式

  • 饿汉式:在类加载时就完成了初始化,所以类加载比较慢,但获取对象的速度快。
  • 懒汉式:在类加载时不初始化,等到第一次被使用时才初始化。

打一个很形象的比喻,有两个很饿的老汉,一个很懒、一个很勤快,懒汉要别人把饭做好送到嘴边的才肯吃;饿汉呢就是拿着碗等着饭熟,饭一熟立马盛饭开吃

那么在程序里面,懒汉式是用的最多,因为它可以减少性能开销。但是懒汉式在单线程环境中没有任何问题,一旦处于多线程环境,那么它是线程不安全的,而如何保证线程安全,各个语言又会有不同的处理方式,本文以JAVA语言为例,来进行介绍单例模式思想实现单例模式的8种方式

首先我们还是以一个非常简单例子为例,介绍单例模式的思想,如果了解了单例模式,可以跳过这小节

1.我是皇帝我独苗

自从秦始皇确立了皇帝这个位置以后,同一时期基本上就只有一个人孤零零地坐在这个位置。这种情况下臣民们也好处理,大家叩拜、谈论的时候只要提及皇帝,每个人都知道指的是谁,而不用在皇帝前面加上特定的称呼,如张皇帝、李皇帝。这一个过程反应到设计领域就是,要求一个类只能生成一个对象(皇帝),所有对象对它的依赖都是相同的,因为只有一个对象,大家对它的脾气和习性都非常了解,建立健壮稳固的关系,我们把皇帝这种特殊职业通过程序来实现。

皇帝每天要上朝接待臣子、处理政务,臣子每天要叩拜皇帝,皇帝只能有一个,也就是一个类只能产生一个对象,该怎么实现呢?对象产生是通过new关键字完成的(当然也有其他方式,比如对象复制、反射等),这个怎么控制呀,但是大家别忘记了构造函数,使用new关键字创建对象时,都会根据输入的参数调用相应的构造函数,如果我们把构造函数设置为private私有访问权限不就可以禁止外部创建对象了吗?臣子叩拜唯一皇帝的过程类图
在这里插入图片描述

只有两个类,Emperor代表皇帝类,Minister代表臣子类,关联到皇帝类非常简单。

public class Emperor {
     private static final Emperor emperor =new Emperor();  //初始化一个皇帝
     private Emperor(){
             //世俗和道德约束你,目的就是不希望产生第二个皇帝}
     public static Emperor getInstance(){
             return emperor;
     }
     //皇帝发话了
     public static void say(){
             System.out.println("我就是皇帝某某某....");
     }
}

通过定义一个私有访问权限的构造函数,避免被其他类new出来一个对象,而Emperor自己则可以new一个对象出来,其他类对该类的访问都可以通过getInstance获得同一个对象。

皇帝有了,臣子要出场

public class Minister {
     public static void main(String[] args) {
             for(int day=0;day<3;day++){
                     Emperor  emperor=Emperor.getInstance();emperor.say();  
             }
             //三天见的皇帝都是同一个人,荣幸吧!
     }
}

臣子参拜皇帝的运行结果如下所示。

我就是皇帝某某某....

我就是皇帝某某某....

我就是皇帝某某某....

臣子天天要上朝参见皇帝,今天参拜的皇帝应该和昨天、前天的一样(过渡期的不考虑,别找茬哦),大臣磕完头,抬头一看,嗨,还是昨天那个皇帝,老熟人了,容易讲话,这就是单例模式。

2 单例模式的定义

单例模式(Singleton Pattern)是一个比较简单的模式,其定义如下:

Ensure a class has only one instance, and provide a global point of access to it.(确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。)

单例模式的通用类图如下图所示

在这里插入图片描述

Singleton类称为单例类,通过使用private的构造函数确保了在一个应用中只产生一个实例,并且是自行实例化的(在Singleton中自己使用new Singleton())

public class Singleton {
     private static final Singleton singleton = new Singleton();        
     //限制产生多个对象
     private Singleton(){
     }
     //通过该方法获得实例对象
     public static Singleton getSingleton(){
             return singleton;
     }  
     //类中其他方法,尽量是static
     public static void doSomething(){
     }
}

3 单例模式的应用

3.1 单例模式的优点

● 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。

● 由于单例模式只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后用永久驻留内存的方式来解决(在Java EE中采用单例模式时需要注意JVM垃圾回收机制)。

● 单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。

● 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。

3.2 单例模式的缺点

● 单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。单例模式为什么不能增加接口呢?因为接口对单例模式是没有任何意义的,它要求“自行实例化”,并且提供单一实例、接口或抽象类是不可能被实例化的。当然,在特殊情况下,单例模式可以实现接口、被继承等,需要在系统开发中根据环境判断。

● 单例模式对测试是不利的。在并行开发环境中,如果单例模式没有完成,是不能进行测试的,没有接口也不能使用mock的方式虚拟一个对象。

● 单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心它是否是单例的,是不是要单例取决于环境,单例模式把“要单例”和业务逻辑融合在一个类中。

3.3 单例模式的使用场景

在一个系统中,要求一个类有且仅有一个对象,如果出现多个对象就会出现“不良反应”,可以采用单例模式,具体的场景如下:

● 要求生成唯一序列号的环境;

● 在整个项目中需要一个共享访问点或共享数据,例如一个Web页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保是线程安全的;

● 创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源;

● 需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式(当然,也可以直接声明为static的方式)。

4 实现单例模式的8种方式

介绍之前我们先看看java种类的加载顺序

类加载(classLoader)机制一般遵从下面的加载顺序

如果类还没有被加载:

  • 先执行父类的静态代码块和静态变量初始化,静态代码块和静态变量的执行顺序跟代码中出现的顺序有关。
  • 执行子类的静态代码块和静态变量初始化。
  • 执行父类的实例变量初始化
  • 执行父类的构造函数
  • 执行子类的实例变量初始化
  • 执行子类的构造函数

同时,加载类的过程是线程私有的,别的线程无法进入。

如果类已经被加载:

静态代码块静态变量不再重复执行,再创建类对象时,只执行与实例相关的变量初始化和构造方法。

static关键字

一个类中如果有成员变量或者方法被static关键字修饰,那么该成员变量或方法将独立于该类的任何对象。它不依赖类特定的实例,被类的所有实例共享,只要这个类被加载,该成员变量或方法就可以通过类名去进行访问,它的作用用一句话来描述就是,不用创建对象就可以调用方法或者变量,这简直就是为单例模式的代码实现量身打造的。

4.1 静态常量(饿汉模式)

这一种是使用静态常量的方式创建实例

public class SingletonTest01 {
    public static void main(String[] args) {
        SingletonClass01 c1 = SingletonClass01.getInstance();
        SingletonClass01 c2 = SingletonClass01.getInstance();
        System.out.println(c1 == c2);
    }
}
class SingletonClass01 {

    // 1. 构造器私有化
    private SingletonClass01() {

    }

    // 2.本类内部创建对象实例
    private final static SingletonClass01 instance = new SingletonClass01();

    // 3. 提供一个公有的静态方法,返回实例对象
    public static SingletonClass01 getInstance() {
        return instance;
    }
}

4.2 静态代码块(饿汉式)

public class SingletonTest02 {
    public static void main(String[] args) {
        SingletonClass02 c1 = SingletonClass02.getInstance();
        SingletonClass02 c2 = SingletonClass02.getInstance();
        System.out.println(c1 == c2);
    }
}

class SingletonClass02 {

    //1. 构造器私有化, 外部不能new
    private SingletonClass02() {

    }

    //2.本类内部创建对象实例
    private static SingletonClass02 instance;

    static { // 在静态代码块中,创建单例对象
        instance = new SingletonClass02();
    }

    //3. 提供一个公有的静态方法,返回实例对象
    public static SingletonClass02 getInstance() {
        return instance;
    }

}

4.3 线程不安全(懒汉式)

public class SingletonTest03 {
    public static void main(String[] args) {
//        System.out.println("单线程创建实例=======");
//        SingletonClass03 c1 = SingletonClass03.getInstance();
//        SingletonClass03 c2 = SingletonClass03.getInstance();
//        System.out.println(c1 == c2);
        System.out.println("多线程创建实例=======");
        //创建10个线程, 在每个 线程中打印单例对象
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    //调用Singleton.getInstance()返回单例对象,打印会输出对象的哈希码
                    System.out.println(SingletonClass03.getInstance());
                }
            }).start();
        }
        //程序运行后,输出单例的哈希码都相同,说明是同一个对象
    }
}

class SingletonClass03 {
    private static SingletonClass03 instance;
    //1. 构造器私有化, 外部不能new
    private SingletonClass03() {
    }

    //提供一个静态的公有方法,当使用到该方法时,才去创建 instance
    //即懒汉式
    public static SingletonClass03 getInstance() {
        if (instance == null) {
            instance = new SingletonClass03();
        }
        return instance;
    }
}

多线程环境测试结果

多线程创建实例=======
cn.mrxccc.singleton.SingletonClass03@576cc207
cn.mrxccc.singleton.SingletonClass03@576cc207
cn.mrxccc.singleton.SingletonClass03@4bd8a307
cn.mrxccc.singleton.SingletonClass03@576cc207
cn.mrxccc.singleton.SingletonClass03@576cc207
cn.mrxccc.singleton.SingletonClass03@576cc207
cn.mrxccc.singleton.SingletonClass03@576cc207
cn.mrxccc.singleton.SingletonClass03@576cc207
cn.mrxccc.singleton.SingletonClass03@576cc207
cn.mrxccc.singleton.SingletonClass03@576cc207

这种是线程不安全的单例模式,从多线程的测试结果可以看出,在创建实例时创建了新的对象,这样的单例模式就是线程不安全单例模式,不推荐该方式

4.4 线程安全,同步方法(懒汉式)

public class SingletonTest04 {
    public static void main(String[] args) {
//        System.out.println("单线程创建实例=======");
//        SingletonClass04 c1 = SingletonClass04.getInstance();
//        SingletonClass04 c2 = SingletonClass04.getInstance();
//        System.out.println(c1 == c2);

        System.out.println("多线程创建实例=======");
        //创建10个线程, 在每个 线程中打印单例对象
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    //调用Singleton.getInstance()返回单例对象,打印会输出对象的哈希码
                    System.out.println(SingletonClass04.getInstance());
                }
            }).start();
        }
        //程序运行后,输出单例的哈希码都相同,说明是同一个对象
    }
}

// 懒汉式(线程安全,同步方法)
class SingletonClass04 {
    private static SingletonClass04 instance;
    //1. 构造器私有化, 外部不能new
    private SingletonClass04() {}

    //提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题
    //即懒汉式
    public static synchronized SingletonClass04 getInstance() {
        if(instance == null) {
            instance = new SingletonClass04();
        }
        return instance;
    }
}

这种是线程安全的单例模式,特点就是给获取实例的方法getInstance()加了一个synchronized关键字,加上锁使其成为一个同步方法,保证了同一时刻只能有一个线程访问并获得实例,但是缺点也很明显,因为synchronized是修饰整个方法,每个线程访问都要进行同步,而其实这个方法只执行一次实例化代码就够了,每次都同步方法显然效率低下,不推荐

4.5双重检查(懒汉式)

public class SingletonTest05 {
    public static void main(String[] args) {
        SingletonClass05 c1 = SingletonClass05.getInstance();
        SingletonClass05 c2 = SingletonClass05.getInstance();
        System.out.println(c1 == c2);
        System.out.println("多线程创建实例=======");
        //创建10个线程, 在每个 线程中打印单例对象
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    //调用Singleton.getInstance()返回单例对象,打印会输出对象的哈希码
                    System.out.println(SingletonClass05.getInstance());
                }
            }).start();
        }
        //程序运行后,输出单例的哈希码都相同,说明是同一个对象
    }
}

// 懒汉式(线程安全,同步方法)
class SingletonClass05 {
    // volatile禁止重排序
    private static volatile SingletonClass05 instance;

    private SingletonClass05() {}

    //提供一个静态的公有方法,加入双重检查代码,解决线程安全问题, 同时解决懒加载问题
    //同时保证了效率, 推荐使用
    public static SingletonClass05 getInstance() {
        if(instance == null) {
            synchronized (SingletonClass05.class) {
                if(instance == null) {
                    instance = new SingletonClass05();
                }
            }

        }
        return instance;
    }
}

这种写法用了两个if判断,也就是Double-Check,并且同步的不是方法,而是代码块,效率较高,是对第四种写法的改进。为什么要做两次判断呢?这是为了线程安全考虑,还是那个场景,对象还没实例化,两个线程A和B同时访问静态方法并同时运行到第一个if判断语句,这时线程A先进入同步代码块中实例化对象,结束之后线程B也进入同步代码块,如果没有第二个if判断语句,那么线程B也同样会执行实例化对象的操作了。

给对象实例加上volatile保证变量的可见性,是为了防止指令重排,这里介绍指令重排,有点偏移文章重点,暂不介绍,关于什么是指令重排可以网上搜一下相关文章

4.6 静态内部类

public class SingletonTest06 {
    public static void main(String[] args) {
//        System.out.println("单线程创建实例=======");
//        SingletonClass06 c1 = SingletonClass06.getInstance();
//        SingletonClass06 c2 = SingletonClass06.getInstance();
//        System.out.println(c1 == c2);
        System.out.println("多线程创建实例=======");
        //创建10个线程, 在每个 线程中打印单例对象
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    //调用Singleton.getInstance()返回单例对象,打印会输出对象的哈希码
                    System.out.println(SingletonClass06.getInstance());
                }
            }).start();
        }
        //程序运行后,输出单例的哈希码都相同,说明是同一个对象
    }
}

// 静态内部类完成:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存
// 推荐使用
class SingletonClass06 {

    //构造器私有化
    private SingletonClass06() {}

    //写一个静态内部类,该类中有一个静态属性 Singleton
    private static class SingletonInstance {
        private final static SingletonClass06 INSTANCE = new SingletonClass06();
    }

    //提供一个静态的公有方法,直接返回SingletonInstance.INSTANCE

    public static SingletonClass06 getInstance() {
        return SingletonInstance.INSTANCE;
    }
}

这是很多开发者推荐的一种写法,这种静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成对象的实例化。

同时,因为类的静态属性只会在第一次加载类的时候初始化,也就保证了SingletonInstance中的对象只会被实例化一次,并且这个过程也是线程安全的。

推荐使用

4.7枚举

这种写法在《Effective JAVA》中大为推崇,它可以解决两个问题:

1)线程安全问题。因为Java虚拟机在加载枚举类的时候会使用ClassLoader的方法,这个方法使用了同步代码块来保证线程安全。

2)避免反序列化破坏对象,因为枚举的反序列化并不通过反射实现。

推荐使用

4.8 静态内部类+防止反射破坏单例

public class SingletonTest08 {
    public static void main(String[] args) throws Exception {
        Class clazz = SingletonClass08.class;
        Constructor constructor = clazz.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        Object c1 = constructor.newInstance();
        Object c2 = SingletonClass08.getInstance();
        System.out.println(c1 == c2);
    }
}

// 静态内部类完成:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存
// 推荐使用
class SingletonClass08 {

    // 构造器私有化
    private SingletonClass08() {
        if (SingletonInstance.INSTANCE != null) {
            throw new RuntimeException("不允许创建多个实例");
        }
    }

    // 写一个静态内部类,该类中有一个静态属性 Singleton
    private static class SingletonInstance {
        private static final SingletonClass08 INSTANCE = new SingletonClass08();
    }

    // 提供一个静态的公有方法,直接返回SingletonInstance.INSTANCE
    public static SingletonClass08 getInstance() {
        return SingletonInstance.INSTANCE;
    }
}

网上的大部分文章介绍单例模式都没有介绍到这一种,这里使用了抛异常的方式防止反射进行创建多个示例

5 总结

至此,关于单例模式的介绍和场景使用已经介绍完了,有任何疑问和沟通讨论的可以给博主留言哦

posted @ 2022-07-22 10:08  狮子挽歌丿  阅读(303)  评论(0编辑  收藏  举报