单例模式

单例模式

单例(Singleton)模式的定义:指一个类只有一个实例,且该类能自行创建这个实例的一种模式。

应用场景:

  1. 当某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池、网络连接池等。
  2. 当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如 Web 中的配置对象、数据库的连接池等。

1. 饿汉模式

使用时已经把类创建完毕。立即加载

//饿汉模式
public class SingletonDemo1 {
    private static SingletonDemo1 instance = new SingletonDemo1();

    private SingletonDemo1() {
    }

    public static SingletonDemo1 getInstance() {
        return instance;
    }
}

2.懒汉模式

延迟加载,在要使用的时候才进行加载。

问题:在多线程的情况下,会出现线程安全的问题。有可能会出现多个实例。

//懒汉模式
public class SingletonDemo2 {
    private static SingletonDemo2 instance = null;

    private SingletonDemo2() {

    }

    public static SingletonDemo2 getInstance() {
        if (instance == null) {
            instance = new SingletonDemo2();
        }
        return instance;
    }
}

3.双重校验锁

//双重校验锁
public class SingletonDemo3 {
    private volatile static SingletonDemo3 instance = null;

    private SingletonDemo3() {

    }

    public static SingletonDemo3 getInstance() {
        if (instance == null) {

            synchronized (SingletonDemo3.class){
                if(instance == null){
                    instance = new SingletonDemo3();
                }
            }
        }

        return instance;
    }
}

使用 volatile保证了多个线程之间的可见性。同时禁止了instance = new SingletonDemoo3的重排序。

在实例化对象的时候底层可以分为3个步骤。

  1. memory = allocate(); //分配对象的内存空间。
  2. ctorInstance(memory); //初始化对象。
  3. instance = memory; //设置instance指向刚分配的内存空间

在重排序的时候,有可能会先运行1->3->2的步骤来提升程序运算速度,这样函数返回的就是null。使用volatile可以禁止重排序。

4.静态内部类

//静态内部类
public class SingletonDemo4 {
    private static class InnerSingleton {
        private static SingletonDemo4 instance = new SingletonDemo4();
    }

    private SingletonDemo4() {
    }

    public static SingletonDemo4 getInstance() {
        return InnerSingleton.instance;
    }
}

总结:外部类加载的时候不会立即加载内部类,调用getInstance是才初始化内部类从而创建instance对象。实例化内部类的时候,虚拟机保证了线程的安全性。

外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化instance,故而不占内存。即当SingletonDemo4第一次被加载时,并不需要去加载InnerSingleton,只有当getInstance()方法第一次被调用时,才会去初始化instance,第一次调用getInstance()方法会导致虚拟机加载InnerSingleton类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。getInstance()方法并没有多次去new对象,故不管多少个线程去调用getInstance()方法,取的都是同一个instance对象,而不用去重新创建。

​ 当getInstance()方法被调用时,InnerSingleton才在SingletonDemo4的运行时常量池里,把符号引用替换为直接引用,这时静态对象instance也真正被创建。

​ 虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。如果在一个类的()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行()方法后,其他线程唤醒之后不会再次进入()方法。同一个加载器下,一个类型只会初始化一次。),在实际应用中,这种阻塞往往是很隐蔽的。

类加载时机:

​ 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的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

这5种情况被称为是类的主动引用,注意,这里《虚拟机规范》中使用的限定词是"有且仅有",那么,除此之外的所有引用类都不会对类进行初始化,称为被动引用。静态内部类就属于被动引用的行列。

5.序列化与反序列化的单例模式实现

/**
 * 序列化和反序列化
 */
public class SingletonDemo5 implements Serializable {
    public User user = new User();
    private static SingletonDemo5 instance = new SingletonDemo5();

    private SingletonDemo5() {
    }

    public static SingletonDemo5 getInstance() {
        return instance;
    }

    public Object readResolve(){
        System.out.println("read resolve");
        return instance;
    }

    public static void main(String[] args) throws Exception {
        SingletonDemo5 sa = SingletonDemo5.getInstance();
        SingletonDemo5 sb = null;

        FileOutputStream fos = new FileOutputStream("a.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(sa);
        oos.flush();
        oos.close();
        fos.close();

        FileInputStream fis = new FileInputStream("a.txt");
        ObjectInputStream ois = new ObjectInputStream(fis);
        sb = (SingletonDemo5) ois.readObject();
        ois.close();
        fis.close();
        System.out.println("两个单例类是否相等:"+(sb == sa));
    }
}

重写了序列化会调用的方法,保证每次序列化返回同一个对象。在readObject会反射调用readResolve方法

序列化原始是使用深拷贝。

6.枚举类单例模式

/**
 * 枚举单例模式
 */
public enum SingletonDemo6 {
    USER;
    
    private User user;

    private SingletonDemo6() {
        user = new User("小明", 22, new User());
    }

    public User getInstance() {
        return user;
    }
}

public static void main(String[] args) {
    User instance1 = SingletonDemo6.USER.getInstance();
    User instance2 = SingletonDemo6.USER.getInstance();
}

枚举类底层保证了user只创建一个。
在枚举类中的常量,每一个都会在底层创建一个类。

参考资料
深入理解单例模式:静态内部类单例原理

你可能感兴趣的:(单例模式)