你确定你知道单例模式吗

318 阅读4分钟

单例模式:毫无疑问就是保证整个应用中对象的实例只有一个。然而单例模式有几种写法,每种写法的优缺点以及问题的由来等你又知道吗?

1、饿汉式
   这种方式比较简单,在类加载的时候完成初始化,避免线程安全问题

package com.my.test.design.singleinstance;


public class SingleInstance1 {

    private static SingleInstance1 s1 = new SingleInstance1();

    private SingleInstance1() {}

    public static SingleInstance1 getInstnce() {
        return s1;
    }
    
}

饿汉式变体
public class SingleInstance1 {

    private static SingleInstance1 s1;

    private SingleInstance1() {}

    // 构造块作用:给对象进行初始化,对象一建立就执行,而且优先于构造函数执行  // 和构造函数区别:构造块是给所有不同对象统一初始化,构造函数是给对应的对象进行初始化    static {
        s1 = new SingleInstance1();
    }

    public static SingleInstance1 getInstnce() {
        return s1;
    }
    
}



2、懒汉式

   在需要的时候才会去初始化,明显的缺点是在单线程的环境下是没问题的,但是在多线程的情况单例
对象可能会被实例化好几次

package com.my.test.design.singleinstance;

public class SingleInstance2 {

    // volatile关键字防止指令重排
    private volatile static SingleInstance2 s2;

    private SingleInstance2() {}

    // 不能保证多线程环境下单例
    public static SingleInstance2 getInstance1() {
        if (s2 == null) {//如果多个线程同时执行到这里那么这个对象会被实例化好几次
            s2 = new SingleInstance2();
        }
        return s2;
    }

    // 方法加锁避免线了程安全问题,但是如果该方法被多个线程频繁调用将会导致程序的性能下降
    // 锁的粒度比较大效率低
    synchronized public static SingleInstance2 getInstance2() {
        if (s2 == null) {
            s2 = new SingleInstance2();
        }
        return s2;
    }

    // 鉴于以上的2个问题才有双重锁机制,双重锁机制并不是完美无缺的,用双重锁机制实例化的对象必须要用
    // volatile 关键字修饰,因为为了提高程序性能编译器和处理器常常会对指令做重排序,指令重排在单线
    // 程环境下没有问题,但是在多线程环境下有可能发生问题   
    public static SingleInstance2 getInstnce3() {
        if (s2 == null){
            synchronized (SingleInstance2.class){
                if (s2 == null){
	            // 其实实例化一个对象在指令层面需要三个步骤
		    // 1、给对象分配内存空间
		    // 2、实例化对象
		    // 3、将实例化对象指向内存空间
		    // 其中2和3可能会出现指令重排,导致线程A其实并没有实例化完成,
		    // 但是该对象已经指向内存,另一个线程B判断
		    // if (s2 == null)为false直接退出,此时的singleton 可能并没有初始化完成
                    // 线程B拿到一个未实例化完成的对象去操作就会有问题
                    s2 = new SingleInstance2();
                }
            }
        }
        return s2;
    }
} 

s2 = new SingleInstance2();// 这句话大概包含三行代码
memory = allocate(); // 1、给对象分配内存空间
ctorInstance(memory) // 2、实例化对象
instance = memory;   // 3、将实例化对象指向内存空间其中2和3可能重排序



3、内部类机制
  这是比较推荐的一种方式,首先外部类无法访问内部类的SINGLETON,只能通过Singleton3的getInstance方法
  初始化时机并不是Singleton3被加载的时候,而是在调用getInstance方法的时候

package com.my.test.design.singleinstance;

public class SingleInstance3 {

    private SingleInstance3 (){}

    public static SingleInstance3 getInstance(){
        return Instance.s3;
    }
    
    private static class Instance {
        private static SingleInstance3 s3 = new SingleInstance3();
    }

} 

4、枚举方式

   类的构造器只能被一个线程在类加载的初始化阶段执行,所以枚举类每个实例在Java堆中只有一个副本

package com.my.test.design.singleinstance;

public enum  SingleInstance4 {

    INSTANCE;

} 

测试类

package com.my.test.design.singleinstance;

public class Test {

    public static void main(String[] args) {

        // 饿汉式
        SingleInstance1 s1 = SingleInstance1.getInstnce();

        // 懒汉式->双重锁
        SingleInstance2 s2 = SingleInstance2.getInstnce3();

        // 内部类
        SingleInstance3 s3 = SingleInstance3.getInstance();

        // 枚举
        SingleInstance4 s4 = SingleInstance4.INSTANCE;
    }
}


    通常我们创建一个对象有以下几个方法

1、new Object()

2、反射

     class.forName("xxxxx")

     类.class;

     new Object.getClass();

3、克隆

4、反序列化

我们可以利用反射破坏单例模式,单例模式的本质是,构造函数私有化,禁止外部通过new的方式创建对象,但是反射可以获得构造器,并且设置setAccessible(true);

无论是饿汉式、懒汉式、还是双重锁机制亦或者是内部类都可以使用反射机制破坏,但是枚举的方式是不行的,有兴趣的话,你们可以自己做一下实验就能看到