单例模式
单例(Singleton)模式的定义:指一个类只有一个实例,且该类能自行创建这个实例的一种模式。
应用场景:
- 当某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池、网络连接池等。
- 当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如 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个步骤。
- memory = allocate(); //分配对象的内存空间。
- ctorInstance(memory); //初始化对象。
- 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也真正被创建。
虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行<clinit>()方法后,其他线程唤醒之后不会再次进入<clinit>()方法。同一个加载器下,一个类型只会初始化一次。),在实际应用中,这种阻塞往往是很隐蔽的。
类加载时机:
JAVA虚拟机在有且仅有的5种场景下会对类进行初始化。
- 遇到new、getstatic、setstatic或者invokestatic这4个字节码指令时,对应的java代码场景为:new一个关键字或者一个实例化对象时、读取或设置一个静态字段时(final修饰、已在编译期把结果放入常量池的除外)、调用一个类的静态方法时。
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化。
- 当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
- 当使用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只创建一个。
在枚举类中的常量,每一个都会在底层创建一个类。
参考资料
深入理解单例模式:静态内部类单例原理