【设计模式】单例模式最常见的几种实现方法以及各自的特点
目录
1.什么是单例模式
单例模式就是保证一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能
2.单例模式解决的问题
多个线程要操作同一对象,要保证对象的唯一性。实例化过程中只实例化一次。
3.单例模式的特点
1,保证一个类只有一个实例;
2,在类中自己实例化自己;
3,向整个系统提供这个实例;
4.单例模式的优点
1,由于单例模式只生成一个实例,所以能减少系统性能的开销;
2,避免对共享资源的多重占用导致的性能损耗,如日志文件,应用配置;
3,提供了对唯一实例的受控访问,加快对象访问速度,比如多线程的线程池的设计,方便对池中的线程进行控制;
5.单例模式的缺点
1,没有接口,扩展困难
2,如果要扩展单例对象,只有修改代码,没有别的途径
6.实现单例模式的方式
6.1 饿汉式
public class HungerySingleton {
//加载的时候就产生的实例对象
private static HungerySingleton instance=new HungerySingleton();
private HungerySingleton(){
}
//返回实例对象
public static HungerySingleton getInstance(){
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(()->{
System.out.println(HungerySingleton.getInstance());
}).start();
}
}
}
线程安全性:在加载的时候已经被实例化,所以只有这一次,线程是安全的。不需要自己加同步处理。基于classloader 加载机制,避免了多线程同步的问题。
是否懒加载(即用到的时候再去实例化):饿汉式没有实现懒加载,没有延迟加载,实例会随着类加载创建,如果这个实例自始至终没有用到,那么就浪费了空间,产生垃圾对象,影响内存性能
性能:如果饿汉生成的数据量不大,那么饿汉式性能比较好
HungerySingleton通过将构造方法限定为private避免了类在外部被实例化,在同一个虚拟机范围内,HungerySingleton的唯一实例只能通过getInstance()方法访问。(事实上,通过Java反射机制是能够实例化构造方法为private的类的,那基本上会使所有的Java单例实现失效。此问题在此处不做讨论,姑且闭着眼就认为反射机制不存在)
6.2 懒汉式(线程不安全写法)
public class HoonSingleton {
private static HoonSingleton instance=null;
private HoonSingleton(){
}
public static HoonSingleton getInstance(){
if(null==instance)
instance=new HoonSingleton();
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(()->{
System.out.println(HoonSingleton.getInstance());
}).start();
}
}
}
线程安全性:不能保证实例对象的唯一性,在多线程环境下是不安全的,所以尽量不要用懒汉模式
是否懒加载:懒汉式是懒加载的,性能比较好
性能:资源利用率高,有延时加载的优势,性能好
6.3 懒汉式(线程安全写法)
6.3.1 锁方法
public class HoonSingleton {
private static HoonSingleton instance=null;
private HoonSingleton(){
}
public static synchronized HoonSingleton getInstance(){
if(null==instance)
instance=new HoonSingleton();
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(()->{
System.out.println(HoonSingleton.getInstance());
}).start();
}
}
}
6.3.2 锁代码块
public class HoonSynSingletonDemo {
private static HoonSynSingletonDemo instance=null;
private HoonSynSingletonDemo(){
}
public static HoonSynSingletonDemo getInstance(){
if(null==instance)
synchronized (HoonSynSingletonDemo.class){
instance=new HoonSynSingletonDemo();
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(()->{
System.out.println(HoonSynSingletonDemo.getInstance());
}).start();
}
}
}
同步方法:
线程安全:实现了线程安全
是否懒加载:也是懒加载
性能:使用synchronized 退化到了串行执行,就不再是真正意义的并发送执行了。性能不好
同步代码块:
有可能出现并发异常,生成两个实例,为了解决这种问题,就要用双锁模式,也就是下面讲的DCL并发模式,进行双重判断。
6.4 Double-Check-Locking 双重校验锁(DCL)
public class DCL {
private static DCL instance=null;
private DCL(){
}
public static DCL getInstance(){
if(null==instance)
synchronized (DCL.class){
if(null==instance)
instance=new DCL();
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(()->{
System.out.println(DCL.getInstance());
}).start();
}
}
}
线程安全:保证了线程安全,也能保证对象的唯一性
是否懒加载:实现了懒加载
性能:比较好,因为用同步代码块比同步方法降低了锁粒度。
但是上面这种写法,有的代码有可能因为指令重排出现空指针异常
6.4.1 DCL指令重排出现空指针异常情况
public class DCL {
int a;
int b;
private static DCL instance=null;
private DCL(){
a = 1;
b = 10;
instance = new DCL();
}
public static DCL getInstance(){
if(null==instance)
synchronized (DCL.class){
if(null==instance)
instance=new DCL();
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(()->{
System.out.println(DCL.getInstance());
}).start();
}
}
}
在构造方法中的成员变量a和b与instance没有数据依赖关系,就有可能出现重排序,使a和b跑到instance实例化下面,就会造成初始化未完成而出现空指针异常。解决办法就是加volatile关键字。
6.4.2 Volatile+Double-check
public class DCL {
int a;
int b;
private volatile static DCL instance=null;
private DCL(){
a = 1;
b = 10;
instance = new DCL();
}
public static DCL getInstance(){
if(null==instance)
synchronized (DCL.class){
if(null==instance)
instance=new DCL();
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(()->{
System.out.println(DCL.getInstance());
}).start();
}
}
}
就加一个volatile修饰,保证在DCL前面的代码不会被指令重排到他的后面就能解决上面出现的指令重排引起的空指针异常问题
6.5 Holder模式 (持有者 静态内部类实现)
public class HolderDemo {
private HolderDemo(){
}
private static class Holder{
private static HolderDemo instance=new HolderDemo();
}
//懒加载
public static HolderDemo getInstance(){
return Holder.instance;
}
}
关于普通内部类和静态内部类的加载时机可以看这一篇:【Java内部类】普通内部类和静态内部类的加载时机
线程安全:保证了线程安全,也能保证对象的唯一性
是否懒加载:只有调用静态内部类的时候这个类才会被实例化,所以也实现了懒加载。
性能:比较好,声明类的时候,成员变量中不声明实例变量,而放到内部静态类中,它不需要使用synchronized关键字就可以保持同步。
这个代码是应用最广泛的一种,结合了懒汉和饿汉模式,还实现了懒加载,性能好而且线程安全。
6.6 内部枚举类实现
public class EnumSingletonDemo {
private EnumSingletonDemo(){
}
private enum EnumHolder{
INSTANCE;
private static EnumSingletonDemo instance=null;
private EnumSingletonDemo getInstance(){
instance=new EnumSingletonDemo();
return instance;
}
}
public static EnumSingletonDemo getInstance(){
return EnumHolder.INSTANCE.instance;
}
}
关于枚举类的相关总结可以看这一篇:【Java枚举】枚举类总结
线程安全:实现简单,枚举本身就是单例,由JVM从根本上提供保障,避免通过反射和反序列化的漏洞
是否懒加载:没有实现懒加载,虽然是内部枚举类,但是它的加载时机和内部类不同,而是和枚举类相同,是在整个外部类初始化的时候,这个内部枚举类中定义的枚举值(INSTANCE)就会被实例化完成
性能:枚举中的成员在枚举类加载的时候就会被实例化,而且只会被实例化一次。性能比较好
《Effectice Java》极力推荐这种单例模式的写法,这种写法是最优雅的。普通的单列实现方式有一个比较大的问题就是如果将单列类序列化后再进行反序列化那么同一个JVM中将存在两个单列类。通过上面的分析枚举类的反序列化不会出现这个问题,所以通过枚举类来实现单例模式是一个很好的选择
7.如何选择上面的几种单例模式
1,占用资源少,不需要延时加载,可以使用枚举式和饿汉式,其中枚举式好于饿汉式;
2,占用资源多,需要延时加载,可以使用内部静态类式,懒汉式,DCL,其中静态内部类式(Holder模式)好于DCL和懒汉式;