设计美感-01设计模式-单例模式

167 阅读4分钟

前言

单例模式是我们经常听到的,但这并不意味着是最简单的设计模式。而是单例模式比较常用
本章主要讲解单例模式,讨论一下单例模式的应用场景和优缺点,以及单例模式的一些实现方式。

单例模式理解

单例模式顾名思义,单例对象的类只能允许一个实例存在。如下代码

Person person1 = new Person();
Person person2 = new Person();
System.out.println(person1 == person2);
# false

我们通过new的方式,创建出来的实例是不一样的。
,许多时候系统只需要拥有一个全局的对象,这样方便我们协调系统的行为。例如获取服务的配置信息存在一个文件中,我们通过单例对象来获取这些配置。再举一个例子,比如redis的jdbc对象我们就可以做成单例。这样项目中操作数据库时不用每次都去初始化连接mysql,目前一些数据库连接池用的就是单例模式,线程池也是。

单例模式优缺点

优点:

  • 在内存中只有一个对象,节省内存空间
  • 避免频繁的创建销毁对象,可以提高性能
  • 避免对共享资源的多重占用,简化访问
  • 为整个系统提供一个全局的访问点 缺点:
  • 不适用于变化频繁的对象
  • 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出
  • 如果实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,这可能会导致对象状态的丢失

单例模式实现

单例模式一般分为两种饿汉式和懒汉式

饿汉式

饿汉方式:提前创建好对象,在类加载的时候就会创建对象
优点:实现简单,没有多线程同步问题 缺点:不管有没使⽤, instance对象一直占着这段内存

public class SingletonHungry {

    private static SingletonHungry instance = new SingletonHungry();

    private SingletonHungry(){
    }

    //返回实例
    public static SingletonHungry getInstance(){
        return instance;
    }
}

#测试
//饿汉式-单例模式测试
SingletonHungry instance = SingletonHungry.getInstance();
SingletonHungry instance2 = SingletonHungry.getInstance();
System.out.println(instance == instance2);#true

懒汉式

懒汉方式:单例实例被延迟加载,即只有在真正使用的时候才会实例化一个对象并交给自己的引用。

第一种方式:
缺点:线程不安全,假设会多个线程同时实例化这个对象。

public class SingletonLazy {

    //构造函数私有化
    private SingletonLazy() {
    }

    /**
     * 缺点:线程不安全,多线程下存在安全问题
     */
    private static SingletonLazy instance;
    public static SingletonLazy getInstance() {
        if (instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }
}

第二种方式:
缺点:synchronized对方法加锁,很大性能开销

/**
 * 第二种⽅式:synchronized 加锁
 * 缺点:采⽤synchronized 对⽅法加锁有很大的性能开销
 */
private static SingletonLazy instance;
public static synchronized SingletonLazy getInstance() {
    if (instance == null) {
        instance = new SingletonLazy();
    }
    return instance;
}

第三种方式: 双重检查:在同步代码中再一次if判断instance是否为null
volatile:由于 synchronized 并不是对 instance 实例进行加锁(因为现在还并没有实例),所以线程在执行完instance = new SingletonLazy()修改 instance 的值后,应该将修改后的 instance 立即写入主存(main memory),而不是暂时存在寄存器或者高速缓冲区(caches)中,以保证新的值对其它线程可见。
还有一个原因是volidate修饰的变量不会进行重排序。

补充:指令重排序:在计算机执行指令的顺序在经过程序编译器编译之后形成的指令序列,一般而言,这个指令序列是会输出确定的结果;以确保每一次的执行都有确定的结果。但是,一般情况下,CPU和编译器为了提升程序执行的效率,会按照一定的规则允许进行指令优化,在某些情况下,这种优化会带来一些执行的逻辑问题,主要的原因是代码逻辑之间是存在一定的先后顺序,在并发执行情况下,会发生二义性,即按照不同的执行逻辑,会得到不同的结果信息。在instance = new SingletonLazy()中,A线程可能因为指令重排未完全构建一个实例时,B线程获取了这个未完全实例。所以采用volatile来禁止指令重排。


private static volatile SingletonLazy instance;

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

后言

可以关注我的博客www.jlovem.cn
可以关注的个人公众号,一起交流,一起成为大牛

本文使用 mdnice 排版