单例模式(Singleton Pattern)

🍀以下内容同步发布在我的个人博客https://www.lvjguo.top😊

1 介绍

定义:确保一个类只有一个实例,并为整个系统提供一个全局访问点 (向整个系统提供这个实例)

类型:创建型

适用场景

  • 1、要求生产唯一序列号。
  • 2、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
  • ··· ···

优点

  • 在内存中只有一个实例,减少了内存的开销
  • 可以避免对资源的多重占用,比如文件的写操作
  • 设置全局访问点,严格控制访问

缺点

  • 没有接口,扩展困难。

结构

aHR0cDovL3N0YXRpYy56eWJ1bHVvLmNvbS9SaWNvMTIzLzhjbGoweDJnM2E3NDJlZXJlYzJ3OTE2di8lRTUlOEQlOTUlRTQlQkUlOEIlRTYlQTglQTElRTUlQkMlOEYlRTclQjElQkIlRTUlOUIlQkUuZ2lm.jpg
单例的三大要素

私有的构造方法(外部无法调用,无法生成多个实例

指向自己实例的私有静态引用

以自己实例为返回值的静态的公有方法

2 单线程环境下单例的实现

在单线程环境下,单例模式根据实例化对象时机的不同,有两种经典的实现:一种是懒汉式单例(延迟加载),一种是 饿汉式单例(立即加载)饿汉式单例在单例类被加载时候,就实例化一个对象并交给自己的引用;而懒汉式单例只有在真正使用的时候才会实例化一个对象并交给自己的引用。

立即加载 : 在类加载初始化的时候就主动创建实例;

延迟加载 : 等到真正使用的时候才去创建实例,不用时不去主动创建。

1) 懒汉式单例

public class LazySingleton {
	//指向自己实例的私有静态引用
    private static LazySingleton lazySingleton;
    
    //私有的构造方法
    private LazySingleton(){}
    
    // 对外提供公有的静态方法获取该对象
    public static LazySingleton getInstance(){
    	//被动创建,在需要使用时才创建实例
        if(lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

从上面代码我们可以看出该方式在成员位置声明Singleton类型的静态变量,并没有进行对象的赋值操作,只有调用getInstance()方法获取Singleton类的对象的时候才创建Singleton类的对象,这样就实现了懒加载的效果。但是,如果是多线程环境,会出现线程安全问题。

2) 饿汉式单例

public class HungrySingleton {

    private static HungrySingleton hungrySingleton = new HungrySingleton();

    private HungrySingleton(){}
    
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

与懒汉式相比,我们可以看出饿汉式单例的实例化出现在类加载时。由于类加载的方式是按需加载的,且只加载一次。因此,在上述单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用;另外,由于类在整个生命周期中只会被加载一次,因此只会创建一个实例,即能够充分保证单例。所以即便是多线程环境,它也是线程安全的。

3 多线程环境下单例的实现

上述我们介绍了基于单线程下两种的单例实现方式,并提出了懒汉式单例在多线程环境下是线程不安全的,而饿汉式天生就是线程安全的。那么我们来深入探究以下几点在多线程环境下的单例实现问题。

1) 传统的懒汉式单例为什么是非线程安全的?

我们要创建一个多线程的环境,通过多线程debug方式手动控制其运行节奏,模拟该可能性。

  • 定义一个线程类,调用单例中的getInstance方法
public class MyThread implements Runnable{
    @Override
    public void run() {
        LazySingleton lazySingleton = LazySingleton.getInstance();
        System.out.println(Thread.currentThread().getName()+": "+lazySingleton);
    }
}
  • 定义一个测试类
public class Test {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new MyThread());
        Thread thread2 = new Thread(new MyThread());
        thread1.start();
        thread2.start();
        System.out.println("end");
    }
}

接下来我们就要通过debug方式模拟多线程操作了,这里需要大家提前学习了解如何使用intelij idea工具的多线程debug调试方法。

  1. 在多线程Debug时需要在断点处勾选Thread
    1 2.png

  2. debug模式启动测试类,有主线程切换到thread-0线程,步入到线程类中
    2 2.png

  3. 步入到线程类,此时thread-0还没有创建对象
    3.png

  4. 切换回thread-1线程,步入到线程类
    4.png

  5. 此时thread-1已经创建了对象,直接返回结果输出
    5.png

  6. 再切换到thread-0线程,创建对象后输出结果
    6.png

  7. 可以看到2个线程所创建的对象不是同一对象
    7.png

上面发生非线程安全的一个显著原因是,会有多个线程同时进入if (lazySingleton == null) {…}语句块的情形发生。当这种这种情形发生后,该单例类就会创建出多个实例,违背单例模式的初衷。因此,传统的懒汉式单例是非线程安全的。

2) 如何使懒汉式单例线程安全?

方法一:synchronized方法

public class LazySingleton {
	//指向自己实例的私有静态引用
    private static LazySingleton lazySingleton;
    
    //私有的构造方法
    private LazySingleton(){}
    
    // 对外提供公有的静态方法获取该对象
    public synchronized static LazySingleton getInstance(){
    	//被动创建,在需要使用时才创建实例
        if(lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

6I0F0U5KZKBGOH11YH.png
此时我们使用多线程debug方式按上面的步骤进行调试时,我们会发现当我们进行到步骤4时,thread-1线程的状态变成了MONITOR,并提示Stepping in thread Thread-1 is blocked by thread Thread-0,此时thread-1线程已经被阻塞。所以此方法使懒汉式单例线程避免了安全性问题。

方法二:synchronized块

public class LazySingleton {
	//指向自己实例的私有静态引用
    private static LazySingleton lazySingleton;
    
    //私有的构造方法
    private LazySingleton(){}
    
    // 对外提供公有的静态方法获取该对象
    public static LazySingleton getInstance(){
    	//被动创建,在需要使用时才创建实例
    	synchronized(LazySingleton.class) {
             if(lazySingleton == null){
                lazySingleton = new LazySingleton();
            }
    	}
    	return lazySingleton;
    }
}

上述两种方式都是对类加同步锁,避免了安全性问题,但也因为对整个类加锁,对性能影响较大。

方法三:双重检查模式(Double-Check idiom)

对于 getInstance() 方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机。由此也产生了一种新的实现模式:双重检查锁模式

public class LazyDoubleCheckSingleton {
    private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton;
    
    private LazyDoubleCheckSingleton(){}
    
    public static LazyDoubleCheckSingleton getInstance(){
        // 第一次判断,如果lazyDoubleCheckSingleton不为null,不进入抢锁阶段,直接返回实例
        if(lazyDoubleCheckSingleton == null){
            synchronized (LazyDoubleCheckSingleton.class){
                // 抢到锁之后再次判断是否为null
                if(lazyDoubleCheckSingleton == null){
                    lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
}

双重检查锁模式是一种非常好的单例实现模式,它通过两次判断单例对象是否被初始化,最终只会有一个线程进入,避免了synchronized放在getInstance()方法上时的性能开销,解决了单例、性能、线程安全问题。

上面的双重检测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会由于JVM在实例化对象的时候会进行优化和指令重排序操作,而出现空指针问题。当我们new LazyDoubleCheckSingleton()时,其实进行了三个操作

1.分配内存给这个对象
2.初始化对象
3.设置lazyDoubleCheckSingleton 指向刚分配的内存地址

9.png
而2和3可能会被重排序,即单例对象先指向刚分配的内存地址,再初始化对象,这样就导致了第一个判断会出现问题,这个就是著名的DCL失效问题。

要解决双重检查锁模式带来空指针异常的问题,只需要使用 volatile 关键字, volatile 关键字可以保证可见性和有序性。

public class LazyDoubleCheckSingleton {
    private static volatile LazyDoubleCheckSingleton lazyDoubleCheckSingleton;
    
    private LazyDoubleCheckSingleton(){}
    
    public static LazyDoubleCheckSingleton getInstance(){
        // 第一次判断,如果lazyDoubleCheckSingleton不为null,不进入抢锁阶段,直接返回实例
        if(lazyDoubleCheckSingleton == null){
            // 加同步锁,保证只有一个线程进入
            synchronized (LazyDoubleCheckSingleton.class){
                // 抢到锁之后再次判断是否为null
                if(lazyDoubleCheckSingleton == null){
                    lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
}

方法四:静态内部类

类加载时机:JAVA虚拟机在有且仅有的5种场景下会对类进行初始化。
1.遇到new、getstatic、setstatic或者invokestatic这4个字节码指令时,对应的java代码场景为:new一个关键字或者一个实例化对象时、读取或设置一个静态字段时(final修饰、已在编译期把结果放入常量池的除外)、调用一个类的静态方法时。
2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化。
3.当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化。
4.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
5.当使用JDK 1.7等动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性(即一个类只有在被使用时才会初始化)。静态属性由于被 static 修饰,保证只被实例化一次,并且严格保证实例化顺序。

public class StaticInnerClassSingleton {
    private static class InnerClass{
        private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
    }
    
    public static StaticInnerClassSingleton getInstance(){
        return InnerClass.staticInnerClassSingleton;
    }
    
    private StaticInnerClassSingleton(){}
    
}

4 破坏单例模式

经过上面的学习,大家可能以为单例模式已经是无懈可击的了,线程必然是安全的。但真的是这样吗?我们知道,如果我们要破坏单例,则必须创建对象,那么我们顺着这个思路走,创建对象的方式无非就是new,clone,反序列化,以及反射

单例模式的首要条件就是构造方法私有化,所以new这种方式去破坏单例的可能性是不存在的
要调用clone方法,那么必须实现Cloneable接口,但是单例模式是不能实现这个接口的,因此排除这种可能性。所以我们来讨论一下反序列化反射如何对单例模式进行破坏。

1) 反序列化破坏单例模式

首先我们为单例模式实现序列化接口,代码如下

public class HungrySingleton implements Serializable{

    private final static HungrySingleton hungrySingleton;

    static{
        hungrySingleton = new HungrySingleton();
    }
    
    private HungrySingleton(){}
    
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

然后我们在测试类中对拿到的对象进行序列化和反序列测试

public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException{
        
        HungrySingleton instance = HungrySingleton.getInstance();

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
        oos.writeObject(instance);

        File file = new File("singleton_file");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));

        HungrySingleton newInstance = (HungrySingleton) ois.readObject();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}

执行结果为

com.lvjguo.design.pattern.creational.singleton.HungrySingleton@312b1dae
com.lvjguo.design.pattern.creational.singleton.HungrySingleton@443b7951
false

可以看出通过对HungrySingleton的序列化与反序列化得到的对象是一个新的对象,这就破坏了HungrySingleton的单例性。

接下来我们来试着打个断点debug一下

image20211019151459267.png

可以看到进入了readObject0()这个方法里,我们进去看看

image20211019151643416.png

继续下一步会走到readOrdinaryObject方法中,可以看到其实反序列化底层也是使用反射帮我们创建了一个新的对象

image20211019152822180.png

但是一定会返回这个新对象吗?是不是我们就不能阻止单例被破坏了呢?

我们再看下面的源码

image20211019160528407.png

在这里有一个上一步由反射生成的obj对象的判断,我们再进入hasReadResolveMethod()这个方法

image20211019155653791.png

从注释可以得出,如果表示的类可序列化或可外部化,并定义一致的readResolve()方法,则返回true。否则,返回false。即如果有readResolve()方法的话,就会通过desc.invokeReadResolve(obj)去反射调用该方法,最终返回的就可能是同一个对象。

看到这里,那现在我们在HungrySingleton类中加上了一个readResolve()方法,该方法返回了hungrySingleton实例,然后重新执行一下来试一试效果:

public class HungrySingleton implements Serializable{

    private final static HungrySingleton hungrySingleton;

    static{
        hungrySingleton = new HungrySingleton();
    }
    
    private HungrySingleton(){}
    
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }

    private Object readResolve(){
        return hungrySingleton;
    }
}
com.lvjguo.design.pattern.creational.singleton.HungrySingleton@312b1dae
com.lvjguo.design.pattern.creational.singleton.HungrySingleton@312b1dae
true

我们加了readResolve()方法后,我们看上面的运行结果,序列化和反序列出来的是同一个对象!

看到这里小伙伴们应该明白了,总结一句话就是:如果想要防止单例被反序列化破坏。就让单例类实现readResolve()方法。

2) 反射破坏单例模式

说完反序列化破坏单例,那现在我们来看看反射如何破坏单例模式:

public class Test {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

        //通过反射创建对象  
        Class objectClass = HungrySingleton.class;
        Constructor constructor = objectClass.getDeclaredConstructor();
        //暴力破解私有构造器
        constructor.setAccessible(true);
        HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();
        HungrySingleton instance = HungrySingleton.getInstance();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}
com.lvjguo.design.pattern.creational.singleton.HungrySingleton@12edcd21
com.lvjguo.design.pattern.creational.singleton.HungrySingleton@34c45dca
false

执行结果为 false,也就是说通过反射也能够破坏单例模式

我们如何应对呢?
即便是通过反射来创建实例,也是调用类中的构造器来实现的,所以我们可以在构造器中做文章。
改造HungrySingleton类中的私有构造器如下:

public class HungrySingleton implements Serializable,Cloneable{

    private final static HungrySingleton hungrySingleton;

    static{
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton(){
        if(hungrySingleton != null){
            throw new RuntimeException("单例构造器禁止反射调用");
        }
    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }

    private Object readResolve(){
        return hungrySingleton;
    }
}

执行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TuLU7q84-1634704922997)(C:\Users\13087\AppData\Roaming\Typora\typora-user-images\image-20211020102046568.png)]

很显然报异常了,这样便防止了这种方法实现的单例模式被反射破坏。

注:

但是这种方式有一个特点,它对类加载时创建对象的类是有效的,比如它和懒汉式的静态内部类实现模式,都会抛出异常。对于其他的懒汉式单例模型,是无法防止被反射破坏的。

5 使用枚举实现单例

《Effective Java》说,枚举是单例模式的最佳实现方式。因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。

public enum EnumSingleton {
    INSTANCE;
    
}

反编译后的代码:

public final class EnumSingleton extends Enum {
   public static final EnumSingleton INSTANCE = new EnumSingleton("INSTANCE", 0);
   // $FF: synthetic field
   private static final EnumSingleton[] $VALUES = new EnumSingleton[]{INSTANCE};
 
   public static EnumSingleton[] values() {
      return (EnumSingleton[])$VALUES.clone();
   }
 
   public static EnumSingleton valueOf(String name) {
      return (EnumSingleton)Enum.valueOf(EnumSingleton.class, name);
   }
 
   private EnumSingleton(String var1, int var2) {
      super(var1, var2);
   }
}

从反编译结果可以看出枚举实现的单例属于饿汉式方式。

6 使用典范

  • Runtime类
public class Runtime {
    private static Runtime currentRuntime = new Runtime();
    public static Runtime getRuntime() {
        return currentRuntime;
    }
     private Runtime() {}
}

以上代码为JDK中Runtime类的部分实现,是饿汉式单例模式。在该类第一次被classloader加载的时候,实例就被创建出来了。一般不能实例化一个Runtime对象,应用程序也不能创建自己的 Runtime 类实例,但可以通过 getRuntime 方法获取当前Runtime运行时对象的引用。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值