单例模式是GoF23中设计模式中最常见的设计模式中,它被用以在多线程环境中保持类的实例只有一个。
饿汉式
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| package xyz.highphone.singleton;
public final class SingleTon1 { private byte[] data = new byte[1024*1024]; private static SingleTon1 instance = new SingleTon1(); private SingleTon1() { } public static SingleTon1 getInstance() { return instance; } }
|
总结
饿汉式单例在多线程环境中能保证实例唯一,是安全的,同时由于在类加载的时候就已经创建实例了,所以在getInstance()的时候只需要返回实例引用即可,效率较高。
但是,由于是类加载的时候进行了new Instance操作,即不是懒加载的,所以单例类的实例变量也会被同时实例化,在堆中分配内存空间,这样可能会造成内存浪费。
懒汉式
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public final class Singleton2 { private byte[] data = new byte[1024*1024];
private static Singleton2 instance = null;
private Singleton2() {
}
public static Singleton2 getInstance() { if(null == instance) instance = new Singleton2(); return instance; }
}
|
总结
懒汉式单例是懒加载方式,在第一次调用getInstance()时,才会去实例化单例。但是懒汉式单例在多线程环境中并不安全,当某线程进来判断null == instance时,会去进行实例化,而如果实例化还没完成,又有第二条线程进来,这是instance还是null,第二条线程也会去进行实例化,所以懒汉式单例在多线程环境可能存在多个实例。
懒汉式 + 同步方法
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public final class Singleton3 { private byte[] data = new byte[1024*1024];
private static Singleton3 instance = null;
private Singleton3() {
}
public static synchronized Singleton3 getInstance() { if(null == instance) { instance = new Singleton3(); } return instance; }
}
|
总结
在懒汉式的基础上,通过给getInstance()方法加锁,可以让单例类在多线程中保证实例唯一,并且也实现了懒加载。但这种在整个方法上加锁的方式,让同一时刻只能有一个线程访问到getInstance()方法,这样非常影响性能。
Double-Check
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| public final class Singleton4 { private byte[] data = new byte[1024*1024];
private static Singleton4 instance = null; private String user; private Address address; private Singleton4() { this.user = "张三"; this.address=new Address(); }
public static Singleton4 getInstance() { if(null == instance) { synchronized(Singleton4.class) { if(null == instance) { instance = new Singleton4(); } } } return instance; } }
|
总结
Double Check方式,即实现了懒加载,又保证了多线程环境下单例类的实例唯一,另外,在同一时刻只有一个线程能访问同步代码块,但是当单例创建后,后续多线程同时访问getInstance()时,instance都不为null,不需要再走同步代码块了,保证了效率。但是这种实现方式在多线程环境中有可能会引起空指针异常。
分析如下:
在某线程第一次进入getInstance()方法的时候,此时instance是null,此线程将获得锁,进行单例实例化操作,此单例的构造函数中,还实例化了另外两个对象user和address,即instance的实例化中,有如下三个步骤
- 实例化instance
- 实例化user
- 实例化address
因为这三个对象的实例化,并没有相互依赖的先后顺序,所以,根据JVM指令重排和Happens-Before原则,此时有可能是instance已完成实例化,但user和address还没完成实例化,而此时恰好有第二条线程调用getInstance()获取instance,对第二条线程来说,instance已经不为null了,返回instance使用,则使用instance调用成员变量user和address就会抛出空指针异常。
Volatile + Double-check
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| public final class Singleton5 { private byte[] data = new byte[1024*1024];
private volatile static Singleton5 instance = null;
private String user; private Address address;
private Singleton5() { this.user = "张三"; this.address=new Address(); }
public static Singleton5 getInstance() { if(null == instance) { synchronized(Singleton5.class) { if(null == instance) { instance = new Singleton5(); } } } return instance; }
}
|
总结
由于JVM指令重排序,让Double-Check方式在多线程环境下可能会出空指针异常,所以我们可以为instance静态变量增加Volatile关键字限定,禁止指令重排,这样,Volatile+Double-Check方式实现的单例模式,可以满足多线程下的唯一实例、懒加载及高效的特性。
Holder方式
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class Singleton6 { private byte[] data = new byte[1024*1024];
private Singleton6() {
}
private static class Holder { private static Singleton6 instance = new Singleton6(); }
public static Singleton6 getInstance() { return Holder.instance; } }
|
总结
- 在Singleton6的成员变量中,并没有instance静态成员变量,而是把instance放到了静态内部类Holder里面,这样,Singleton6类加载时其静态内部类Holder不会被加载,所以不会创建instance实例,当Holder被主动引用的时候才会创建instance实例,这样实现单例的方式满足了懒加载的特性。
- Singleton6的创建过程会在程序编译时期收集至 ()方法中,而在《深入理解JAVA虚拟机》有这样的描述:虚拟机会保证一个类的()方法在多线程的环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。所以,Holder方式实现的单例模式,是可以满足多线程环境中的唯一实例,且高效率的特性的。Holder方式的单例设计是最好的设计之一,也是目前使用比较广的设计之一。
枚举方式
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| public class Singleton7 { private byte[] data = new byte[1024*1024];
private Singleton7() {
} private enum EnumHolder { INSTANCE; private Singleton7 instance; EnumHolder() { this.instance = new Singleton7(); }
private Singleton7 getSingleton7() { return instance; }
}
public static Singleton7 getInstance() { return EnumHolder.INSTANCE.getSingleton7(); } }
|
总结
枚举类型不允许被继承,同样是线程安全的,而且只能被实例化一次,同时instance定义在单例类内部的枚举类型中,也满足了懒加载特性。并且枚举类型只能被实例化一次,所以保证了单例不会被反射机制破坏掉,所以,从某种意义上来说,通过枚举来实现单例是最好的设计方式了。
本文链接:https://highphone.xyz/f1601c3e.html