重学设计模式之单例模式
创建型模式的工作原理
创建型模式提供了一种创建对象的机制,抽象实例化的过程,隐藏了对象的创建细节,对外只提供一个通用接口,能够提升已有代码的灵活性和可复⽤性。创建型模式有五种:单例模式、工厂模式、抽象工厂模式、建造者模式、原型模式。
单例模式
定义
单例模式(Singleton Pattern)提供了一种创建对象的方式,实现某个类全局只有一个实例的场景。这个类负责创建自己的对象,同时确保只有单个对象被创建,并提供一种访问其唯一的对象的方法。
结合我们实际的开发经验,由于单例模式在内存中只有一个对象实例,减少了内存开支,特别是某些对象需要频繁地创建、销毁时,从而提升整体的代码性能。
通俗点讲,单例模式就像古代的皇帝,大臣们每天上朝面对的都是同一个皇帝(除非皇帝驾崩,才会改朝换代)。如果每天都是新皇帝,那不就朝令夕改,天下打乱了嘛!那要怎么实现呢?一个皇帝就是一个对象,我们都知道new关键字会调用类的构造方法完成实例化。此时,如果我们将构造方法设置为private访问权限,外部就无法创建对象了。
应用场景
1、要求生产唯一序列号。
2、需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式(也可以直接声明为static的方式)。
3、spring中⼀个单例模式bean的⽣成和使⽤(spring是通过注册表实现单例的,Ioc容器维护了一个bean表格,当需要一个单例bean时,从表格中获取,没有获取到的,向表格注册一个新的bean)。
4、数据库的连接池不会反复创建
优点
1、由于单例模式在JVM中只创建了一个实例,减少了内存的占用,特别是在代码中,对象需要频繁地创建、销毁时,能起到不错的性能优化。
2、单例模式可以避免对资源的多重占用,如:在读写文件时,避免被多个进程打开,造成读写冲突
缺点
单例模式需要主动实例化,一般不实现接口,难扩展。与单一职责原则冲突,一个类应该只关注内部逻辑,而不关心是否是单例的。
如何破解单例模式
1、通过反射破解(反射如同bug一般的存在,在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性)
2、通过反序列化
具体实现
单例模式的实现方式比较多,主要在实现上区别是否⽀持懒汉模式、是否线程安全。当然有些场景会直接使⽤ static 静态类或属性和⽅法的⽅式进⾏修饰,供外部调⽤。
1、懒汉模式(线程不安全)
public class Singleton_1 {
private static Singleton_1 singleton_1;
private Singleton_1() {
}
public static Singleton_1 getInstance() {
if (Objects.isNull(singleton_1)) {
singleton_1 = new Singleton_1();
}
return singleton_1;
}
}
以上代码是最基本的实现方式,实现了懒加载,构造方法私有化,外部无法访问,只能通过调用getInstance方法去获得对象实例。但是这种实现最大的问题是:在多线程环境下,可能会创建多个实例对象,从而没有达到单例的要求,所以不推荐使用该方式。
2、懒汉模式(线程安全)
public class Singleton_2 {
private static Singleton_2 singleton;
private Singleton_2() {
}
public static synchronized Singleton_2 getInstance() {
if (Objects.isNull(singleton)) {
singleton = new Singleton_2();
}
return singleton;
}
}
大家仔细看的话,唯一的区分是用synchronized修饰了getInstance方法。synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行(后期会专门讲多线程与锁,请大家持续关注),但是注意synchronized加锁会影响方法的执行效率
3、饿汉模式(线程安全)
public class Singleton_3 {
private static Singleton_3 singleton = new Singleton_3();
private Singleton_3() {
}
public static Singleton_3 getInstance() {
return singleton;
}
}
JVM在加载类时,是按需加载,且只加载一次。采用上述方式,在加载Singleton_3类时,会立刻完成对singleton对象的实例化,由于Singleton_3在整个生命周期中只会被加载一次,因此只会创建一个实例,保证了线程安全。后续如果有外部需要,直接获得singleton实例对象即可。
4、双重锁校验(线程安全)
public class Singleton_4 {
private static volatile Singleton_4 singleton;
private Singleton_4() {
}
public static Singleton_4 getInstance() {
if (Objects.isNull(singleton)) {
synchronized (this) {
if (Objects.isNull(singleton)) {
singleton = new Singleton_4();
}
}
}
return singleton;
}
}
双重锁校验,相较与方法二(synchronized直接修饰方法,每次调用getInstance方法时,都需要获得monitor监视器锁才能执行,效率比较低),双重锁校验实现方式只有第一次调用需要同步创建对象,一旦完成初始化之后都不需要获得锁,可以直接返回singleton对象,减少了耗时。
5、静态内部类(线程安全)
public class Singleton_5 {
private static class SingletonHandler {
private static final Singleton_5 INSTANCE = new Singleton_5();
}
private Singleton_5() {
}
public static Singleton_5 getInstance() {
return SingletonHandler.INSTANCE;
}
}
使⽤类的静态内部类实现的单例模式,既保证了线程安全有保证了懒加载,同时不会因为加锁的⽅式耗费性能。这种方式实现更简单,对静态域使用延迟初始化,相较与方法三,避免了内存的浪费。
6、枚举单例
public enum Singleton_06 {
INSTANCE;
public void doSomething() {
}
}
利用枚举的特性,让JVM来帮我们保证线程安全和单一实例的问题。除此之外,写法还特别简单。
总结
虽然单例模式是一个很常见的设计模式,但是如何用好单例模式,还是需要我们自己去实践中总结,按需选择最优的实现方式。