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

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

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

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

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

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

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

1.我是皇帝我独苗

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

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

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

只有两个类,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

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

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

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

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

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

2 单例模式的定义

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

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

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

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

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;
    }
}

多线程环境测试结果

&#x591A;&#x7EBF;&#x7A0B;&#x521B;&#x5EFA;&#x5B9E;&#x4F8B;=======
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 总结

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

Original: https://www.cnblogs.com/mrxccc/p/16504757.html
Author: 狮子挽歌丿
Title: 设计模式-单例模式介绍+8种实现方式

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/588652/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • vue找不到页面自定义404页面

    在vue项目中,如果不做路由处理的话,用户可以直接在url随意输入跳转页面, 默认的时候我们是并没有什么处理。 现在需要做一个自定义的404找不到页面的处理方式。 1.在route…

    Java 2023年6月14日
    071
  • 2022-8-9 第六组 输入输出流

    IO流 Java中输入/输出流常用的流: &#x5B57;&#x8282;&#x8F93;&#x5165;&#x6D41; &#x5…

    Java 2023年6月13日
    080
  • csv.writer写入文件有多余的空行

    在用csv.writer写入文件的时候发现中间有多余的空行。 最早打开方式只是 ‘w’,会出现多余的空行,网上建议使用binary形式 ‘wb&…

    Java 2023年5月29日
    0112
  • springboot学习

    springboot学习 官方文档:https://spring.io/projects/spring-boot 1、简介 1.1、什么是spirngboot? springboo…

    Java 2023年6月5日
    090
  • Java连载150-NIO详解(一)

    一、IO原理 1.底层原理 操作系统在进行IO的时候,实际上并不是即时操作,它们是通过缓冲区的,也就是说,我们读写文件都是通过一个中介来进行的。读系统就是把内核缓存区的内容复制到进…

    Java 2023年6月13日
    083
  • JDK成长记7:3张图搞懂HashMap底层原理!

    HashMap基本原理和优缺点 HashMap基本原理和优缺点 一句话讲, HashMap底层数据结构,JDK1.7数组+单向链表、JDK1.8数组+单向链表+红黑树。 HashM…

    Java 2023年6月5日
    092
  • java_序列化和反序列化

    1.序列化的作用: 序列化的原本意图是希望对一个Java对象作一下”变换”,变成字节序列,这样一来方便持久化存储到磁盘,避免程序运行结束后对象就从内存里消失…

    Java 2023年6月5日
    068
  • SpringBoot扩展接口- Bean实例化前后扩展点

    常用的扩展接口: 1、ApplicationContextAware:获取应用上下文 2、BeanPostProcessor 接口在对象实例化后, 初始化方法调用前后做前置处理和后…

    Java 2023年5月30日
    090
  • SpringBoot 项目部署(初级)

    之前的项目一直在本地电脑上写,最近需要将项目部署到服务器上进行联调测速度。于是,在网上搜集资料后简单的进行一下总结。 _由于本次打包部署是为了测试,于是很多内容做的还不算详尽 ,_…

    Java 2023年6月7日
    084
  • JS 模块化-03 AMD 规范与 Require JS

    1 AMD 规范介绍 AMD 规范,全称 Asynchronous Module Definition,异步模块定义,模块之间的依赖可以被异步加载。 AMD 规范由 Common …

    Java 2023年6月16日
    091
  • 设计模式之策略模式

    在一个收银系统中,如果普通用户、中级会员、高级会员分别对应着不同的优惠策略,常规编程就要使用一系列的判断语句,判断用户类型,这种情况下就可以使用策略模式。 一、概念理解 策略模式的…

    Java 2023年6月8日
    0103
  • Springboot+Websocket+JWT实现的即时通讯模块

    场景 目前做了一个接口:邀请用户成为某课程的管理员,于是我感觉有能在用户被邀请之后能有个立马通知他本人的机(类似微博、朋友圈被点赞后就有立马能收到通知一样),于是就琢磨琢磨搞了一套…

    Java 2023年6月7日
    092
  • 模块(类)之间解耦利器:EventPublishSubscribeUtils 事件发布订阅工具类

    如果熟悉C#语言的小伙伴们一般都会知道委托、事件的好处,只需在某个类中提前定义好公开的委托或事件(委托的特殊表现形式)变量,然后在其它类中就可以很随意的订阅该委托或事件,当委托或事…

    Java 2023年6月9日
    092
  • Spring-Cloud-Ribbon学习笔记(二):自定义负载均衡规则

    Ribbon自定义负载均衡策略有两种方式,一是JavaConfig,一是通过配置文件(yml或properties文件)。 需求 假设我有包含A和B服务在内的多个微服务,它们均注册…

    Java 2023年5月30日
    087
  • 干货分享:ASP.NET CORE(C#)与Spring Boot MVC(JAVA)异曲同工的编程方式总结

    本文目的是通过全面简述C# 与JAVA 在基础语法以及ASP.NET CORE 与 Spring Boot MVC的在框架规范、部署、运行的异曲同工的实现方式,让大家更多的了解C#…

    Java 2023年5月29日
    0175
  • 【校招VIP】【约起来】高校大学生自己的商业项目|产品脑图的重要性:活动模型的细节

    VIP的服务不一样的校招,大家好,我是校招VIP的大拿老师。今天看一下【在线实习】这个课程体系的【约起来】项目。 这是高校陌生人活动平台第一个产品发布模块(产品)的第一个课程,分成…

    Java 2023年6月5日
    0100
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球