【设计模式】单例模式

举报
living 发表于 2021/01/28 17:08:28 2021/01/28
【摘要】 一、简介1、概述单例模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。注意:单例类只能有一个实例。单例类必须自己创建自己的唯一实例。单例类必须给所有其他对象提供这一实例。使用时不能用反射模式创建单例,否则会实例化一个新的对象使用懒单例模式时注意线程安全问题饿单例模式和懒单例模式构造方法都是...

一、简介

1、概述

单例模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

注意:

  • 单例类只能有一个实例。
  • 单例类必须自己创建自己的唯一实例。
  • 单例类必须给所有其他对象提供这一实例。
  • 使用时不能用反射模式创建单例,否则会实例化一个新的对象
  • 使用懒单例模式时注意线程安全问题
  • 饿单例模式和懒单例模式构造方法都是私有的,因而是不能被继承的,有些单例模式可以被继承(如登记式模式) 

2、适用场景

  • 需要频繁实例化然后销毁的对象。
  • 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
  • 有状态的工具类对象。
  • 频繁访问数据库或文件的对象。 

3、应用场景举例

  • 资源共享的情况下,避免由于资源操作时导致的性能或损耗,避免并发操作冲突等。如应用程序的日志应用,共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加;应用的配置对象的读取,配置文件是共享的资源;Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例(一个文件系统)来进行。
  • 一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,只能有一个PrinterSpooler,在输出的时候不能两台打印机打印同一个文件。
  • 网站的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来,否则难以同步。 
  • 创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等,主要是节省打开或者关闭连接的效率损耗
  • 多线程的线程池

4、优点

  • 减少内存开销。只存在一个实例,针对对象需要频繁地创建销毁的场景节省内存开支;只创建一次实例,针对实例化对象产生需要比较多的资源时,能极大优化
  • 提供了对唯一实例的受控访问。
  • 避免对资源的多重占用(比如写文件操作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作)
  • 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如,可以设计一个单例类,负责所有数据表的映射处理。

5、缺点

  • 不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
  • 由于单利模式中没有抽象层(没有接口,不能继承),因此单例类的扩展有很大的困难。
  • 单例类的职责过重,在一定程度上违背了“单一职责原则”。
  • 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。 

二、实现方式

1、饿汉式

静态变量直接实例化实例,确保在类加载的时候就创建唯一的实例。

优点:(1)线程安全。唯一一次实例化是在类加载的时候创建静态变量做的,以空间换时间,故不存在线程安全问题。

(2)在类加载的同时已经创建好一个静态对象,调用时反应速度快

(3)没有加锁,执行效率会提高。

缺点:(1)类加载时就初始化,可能getInstance()永远不会执行到,但执行该类的其他静态方法或者加载了该类(class.forName),那么这个实例仍然初始化,资源利用率不高,浪费内存。

/**
 *  饿汉式
 */
public class SingletonObject1 {
    // 1、构造方法私有化:确保其它类无法调用构造方法,从而无法使用new进行实例化
    private SingletonObject1 () {
    }
    // 2、静态的实例:由于是类的变量,因此该实例仅有1份;
    // 类变量在加载类的时候就会初始化创建;
    // 只要加载了该类,不管是否调用getInstance,该实例都会进行初始化(缺点)
    public static SingletonObject1 singletonObject1 = new SingletonObject1();
    // 3、外部调用获取实例的方法:注意要static
    public static SingletonObject1 getInstance() {
        return singletonObject1;
    }
}


2、懒汉式

静态变量里不实例化,单例实例在第一次被使用时才构建,延迟初始化。 

优点:避免了饿汉式的那种在没有用到的情况下创建事例,资源利用率高,不执行getInstance()就不会被实例,可以执行该类的其他静态方法。 

缺点:如果getInstance不加sybchronited关键字,则为线程非安全版本,在多线程下无法保证单例。

添加了锁能确保多线程下线程安全,保证单例,但加锁会影响效率,只有该类第一次实例化时需要同步,实例化后不需要同步。

/**
 * 懒汉式
 */
public class SingletonObject2 {
    // 1、构造方法私有化
    private SingletonObject2 () {
    }
    // 2、不在类加载时就初始化
    private static SingletonObject2 singletonObject2 = null;
    // 3、保证线程安全的关键:添加synchronized关键字,否则就不是线程安全,在多线程下就不是真实的单例了
    // 引入缺点,获取实例串行化,多个线程同时调用getInstance的时候会排队获取,性能下降
    public static synchronized SingletonObject2 getInstance() {
        if (singletonObject2 == null) { // 当第一个进入getInstance时才会实例化对象,提高了资源利用率(懒加载)
            singletonObject2 = new SingletonObject2();
        }
        return singletonObject2;
    }
}


3、双重校验式

懒汉式要解决线程安全问题-->getInstance加锁-->带来效率低下问题,getInstance串行化执行,实际除第一次初始化需要保证同步,后续不需要

-->双重校验式去除getInstance方法中的锁,在if里面加锁同时再加一层判断-->引入可见性问题-->在静态实例中添加volatile关键字解决

优点:资源利用率高,不执行getInstance()就不被实例,可以执行该类其他静态方法;相对懒汉式效率高
缺点 :第一次加载时反应不快

/**
 * 双重校验式
 */
public class SingletonObject3 {
    // 1、私有构造
    private SingletonObject3() {}
    // 2、加static:唯一一个实例
    // 置为null:类加载时不实例化,懒加载
    // 加volatile关键字,避免因可见性带来的问题
    private volatile static SingletonObject3 singletonObject3 = null;
    public static SingletonObject3 getInstance() {
        // 当singletonObject3未实例化时,线程才会进入到if里面
        // 一旦有线程对singletonObject3进行了实例化,则后续再调用getInstance的线程都不会再进入if中
        if (singletonObject3 == null) {
            // 当多个线程进入了if里面准备对singletonObject3进行初始化,此时需要锁将这些线程串行化执行
            synchronized (SingletonObject3.class) {
                // 串行化下,先抢到锁的线程已经初始化完了,后面的线程进来需要再判断一下,避免重复实例化
                if (singletonObject3 == null) {
                    singletonObject3 = new SingletonObject3();
                }
            }
        }
        return singletonObject3;
    }
}


图解:

(1)初次有线程调用SingleObject3的getInstance方法,则由于singleObject3未被实例化,所以都进入了if,由于遇到了synchronized,于是接下来的代码变成串行化执行

(2)假设线程2抢到了锁,进入synchronized块完成singleObject3的实例化

(3)接着线程1抢到锁,进入synchronized块,由于线程2已经触发了singleObject3的实例化,因此判断到singleObject3不为空,跳过实例化(想一想,如果不加这重校验,是不是线程1和3都会继续对singleObject3进行实例化)

所以双重校验的关键点之一,在synchronized块里再加一个判断作用就是多个线程准备对singleObject3进行实例化时,避免后抢到锁的线程重复实例化

由于jvm存在乱序执行功能,因此也会出现线程不安全的情况。

具体分析如下:

singletonObject3 = new SingletonObject3();

实例化的步骤其实在jvm的执行分为三步:

a.在堆内存开辟内存空间。
b.在堆内存中实例化SingletonObject3里面的各个参数。
c.把对象指向堆内存空间。--此时对象就不为null了

假设线程2抢到锁执行new,然后线程4执行getInstance方法,此时线程2和线程4均处于可运行状态,由于jvm存在乱序执行功能,所以可能在b还没执行时就先执行了c,假如此时切换到线程4执行,那么线程4就会判断到对象不为空,直接拿去用了,此时就出现问题

解决办法,也是双重校验的关键点之一,singleObject3对象加volatile关键字,volatile会在编译时加lock,禁止了指令重排序,从而解决上述问题

(4)后续进来的线程,因为对象已经实例化了,则不再进入if,也就不会碰到synchronized,也就解决了懒汉式对象实例化后,调用getInstance非必要的抢锁串行化执行的问题

4、登记式(静态内部类)

静态内部类相当于外部类的非静态属性,加载外部类时不会触发静态内部类的加载,当显式调用 getInstance 方法时,才会显式装载 SingleHandle类,从而实例化 instance。

优点:资源利用率高,不执行getInstance()不被实例-->弥补了饿汉式的缺点,同时不需要像双重校验那样麻烦,且不像懒汉式那样效率低下
缺点:第一次加载时反应不够快;由于是静态内部类的形式去创建单例的,故外部无法传递参数进去

/**
 * 登记式(静态内部类)
 */
public class SingletonObject4 {
    // 1、私有构造
    private SingletonObject4 () {}
    // 2、私有静态内部类,在里面设定实例化的静态变量(与饿汉的不同之处)
    private static class SingleHandle{
        public static SingletonObject4 singletonObject4 = new SingletonObject4();
    }
    // 3、获取实例时,直接取静态内部类中的静态变量,即唯一实例
    public static SingletonObject4 getInstance() {
        return SingleHandle.singletonObject4;
    }
}

静态内部类的方式能保证线程安全的原因就在于JVM默认静态字段初始化是加同步锁的

在多线程开发中,为了解决并发问题,主要是通过使用synchronized来加互斥锁进行同步控制, 但是在某些情况下,JVM已经隐含的为您执行了同步,这些情况下就不用自己再来进行同步控制了。 这些情况包括: 
(1)由静态初始化器(在静态字段上或static{}块中的初始化器)初始化数据时;
(2)访问final字段时; 
(3)在创建线程之前创建对象时; 
(4)线程可以看见它将要处理的对象时。

5、枚举

线程安全,非懒加载

/**
 * 枚举类
 */
public enum Singleton {
    INSTANCE;
    public void showMsg () {
        System.out.println("Hello World!");
    }
}

6、简单校验多线程单例的方法

测试正确的单例,应该打印一样的类。假如用下面代码测试懒汉式,且getInstance不加synchronized关键字,则会出现打印不同的类

class MyTest{
    public static void main(String[] args) {
        IntStream.rangeClosed(1, 100).forEach(i->new Thread(){
            @Override
            public void run() {
                System.out.println(SingletonObject2.getInstance());
            }
        }.start());
    }
}

7、经验之谈

(1)一般情况用饿汉式

(2)不建议用懒汉式

(3)明确使用懒加载时,才用静态内部类的方式

(4)有传参需求,及懒加载需求,才用双重校验方式

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。