高频面试题:单列模式的6种实现方式。
整理分享不易,点个赞鸭~
目录:
定义: 确保一个类只有一个实例,并提供该实例的全局访问点。
这样做的好处是:有些实例,全局只需要一个就够了,使用单例模式就可以避免一个全局使用的类,频繁的创建与销毁,耗费系统资源。
简单来说就是,单例类的构造方法不让其他人修改和使用;并且单例类自己只创建一个实例,这个实例,其他人也无法修改和直接使用;然后单例类提供一个调用方法,想用这个实例,只能调用。这样就确保了全局只创建了一次实例。
实现:
public class Singleton { private static Singleton uniqueInstance; private Singleton() { } public static Singleton getUniqueInstance() { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } return uniqueInstance; } }
说明: 先不创建实例,当第一次被调用时,再创建实例,所以被称为懒汉式。
优点: 延迟了实例化,如果不需要使用该类,就不会被实例化,节约了系统资源。
缺点: 线程不安全,多线程环境下,如果多个线程同时进入了 if (uniqueInstance == null) ,若此时还未实例化,也就是uniqueInstance == null,那么就会有多个线程执行 uniqueInstance = new Singleton(); ,就会实例化多个实例;
实现:
public class Singleton { private static Singleton uniqueInstance = new Singleton(); private Singleton() { } public static Singleton getUniqueInstance() { return uniqueInstance; } }
说明: 先不管需不需要使用这个实例,直接先实例化好实例 (饿死鬼一样,所以称为饿汉式),然后当需要使用的时候,直接调方法就可以使用了。
优点: 提前实例化好了一个实例,避免了线程不安全问题的出现。
缺点: 直接实例化好了实例,不再延迟实例化;若系统没有使用这个实例,或者系统运行很久之后才需要使用这个实例,都会操作系统的资源浪费。
实现:
public class Singleton { private static Singleton uniqueInstance; private static singleton() { } private static synchronized Singleton getUinqueInstance() { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } return uniqueInstance; } }
说明: 实现和 线程不安全的懒汉式 几乎一样,唯一不同的点是,在get方法上 加了一把 锁。如此一来,多个线程访问,每次只有拿到锁的的线程能够进入该方法,避免了多线程不安全问题的出现。
优点: 延迟实例化,节约了资源,并且是线程安全的。
缺点: 虽然解决了线程安全问题,但是性能降低了。因为,即使实例已经实例化了,既后续不会再出现线程安全问题了,但是锁还在,每次还是只能拿到锁的线程进入该方***使线程阻塞,等待时间过长。
实现:
public class Singleton { private volatile static Singleton uniqueInstance; private Singleton() { } public static Singleton getUniqueInstance() { if (uniqueInstance == null) { synchronized (Singleton.class) { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } } return uniqueInstance; } }
说明: 双重检查数相当于是改进了 线程安全的懒汉式。线程安全的懒汉式 的缺点是性能降低了,造成的原因是因为即使实例已经实例化,依然每次都会有锁。而现在,我们将锁的位置变了,并且多加了一个检查。 也就是,先判断实例是否已经存在,若已经存在了,则不会执行判断方法内的有锁方法了。 而如果,还没有实例化的时候,多个线程进去了,也没有事,因为里面的方法有锁,只会让一个线程进入最内层方法并实例化实例。如此一来,最多最多,也就是第一次实例化的时候,会有线程阻塞的情况,后续便不会再有线程阻塞的问题。
为什么使用 volatile 关键字修饰了 uniqueInstance 实例变量 ?
uniqueInstance = new Singleton(); 这段代码执行时分为三步:
正常的执行顺序当然是 1>2>3 ,但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1>3>2。
单线程环境时,指令重排并没有什么问题;多线程环境时,会导致有些线程可能会获取到还没初始化的实例。
例如:线程A 只执行了 1 和 3 ,此时线程B来调用 getUniqueInstance(),发现 uniqueInstance 不为空,便获取 uniqueInstance 实例,但是其实此时的 uniqueInstance 还没有初始化。
解决办法就是加一个 volatile 关键字修饰 uniqueInstance ,volatile 会禁止 JVM 的指令重排,就可以保证多线程环境下的安全运行。
优点: 延迟实例化,节约了资源;线程安全;并且相对于 线程安全的懒汉式,性能提高了。
缺点: volatile 关键字,对性能也有一些影响。
实现:
public class Singleton { private Singleton() { } private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getUniqueInstance() { return SingletonHolder.INSTANCE; } }
说明: 首先,当外部类 Singleton 被加载时,静态内部类 SingletonHolder 并没有被加载进内存。当调用 getUniqueInstance() 方法时,会运行 return SingletonHolder.INSTANCE; ,触发了 SingletonHolder.INSTANCE ,此时静态内部类 SingletonHolder 才会被加载进内存,并且初始化 INSTANCE 实例,而且 JVM 会确保 INSTANCE 只被实例化一次。
优点: 延迟实例化,节约了资源;且线程安全;性能也提高了。
实现:
public enum Singleton { INSTANCE; //添加自己需要的操作 public void doSomeThing() { } }
说明: 默认枚举实例的创建就是线程安全的,且在任何情况下都是单例。
优点: 写法简单,线程安全,天然防止反射和反序列化调用。
private Object readResolve() throws ObjectStreamException{ return singleton; }
应用场景举例:
使用场景总结:
第一种:饿汉模式(线程安全)
public class Single2 { private static Single2 instance = new Single2(); private Single2(){ System.out.println("Single2: " + System.nanoTime()); } public static Single2 getInstance(){ return instance; } }
第二种:懒汉模式 (如果方法没有synchronized,则线程不安全)
public class Single3 { private static Single3 instance = null; private Single3(){ System.out.println("Single3: " + System.nanoTime()); } public static synchronized Single3 getInstance(){ if(instance == null){ instance = new Single3(); } return instance; } }
第三种:懒汉模式改良版(线程安全,使用了double-check,即check-加锁-check,目的是为了减少同步的开销)
public class Single4 { private volatile static Single4 instance = null; private Single4(){ System.out.println("Single4: " + System.nanoTime()); } public static Single4 getInstance(){ if(instance == null){ synchronized (Single4.class) { if(instance == null){ instance = new Single4(); } } } return instance; } }
第四种:利用私有的内部工厂类(线程安全,内部类也可以换成内部接口,不过工厂类变量的作用于要改为public了。)
public class Singleton { private Singleton(){ System.out.println("Singleton: " + System.nanoTime()); } public static Singleton getInstance(){ return SingletonFactory.singletonInstance; } private static class SingletonFactory{ private static Singleton singletonInstance = new Singleton(); } }
通过单例模式可以保证系统中一个类只有一个实例而且该实例易于被外界访问,从而方便对实例个数的控制并节约系统资源。如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决办法。
public class EagerSingleton { //可以由名知意,太饥饿了,一大早就准备好了实例等待使用,但很有可能一直没人使用 private static final EagerSingleton instance = new EagerSingleton(); private EagerSingleton() {} public static EagerSingleton getInstance() { return instance; } }
public class LazySingleton { //等到人家使用才生产一个实例,所以叫懒汉模式 private static LazySingleton instance = null; private LazySingleton() {} public static LazySingleton getInstance() { if(instance == null) { instance = new LazySingleton(); } return instance; } }
懒汉式单例类在实例化时,必须处理好多个线程同时首次引用此类时的访问限制问题。
为了实现懒汉式单例类的线程安全,必须加锁 synchronized 实现单例,但加锁会影响效率。
public class LazySingleton { private static LazySingleton instance = null; private LazySingleton() {} public static synchronized LazySingleton getInstance() { if(instance == null) { instance = new LazySingleton(); } return instance; } }
在保证安全的前提下保持高性能的实现方式
public class LazySingleton { // 防止指令重排序以及保证变量在多线程运行时的可见性 private volatile static LazySingleton instance = null; private LazySingleton() {} public static LazySingleton getInstance() { // 由于单例模式只需创建一次实例即可 // 所有当已经创建一个实例之后,再次调用就不需要进入同步代码块中了 // 避免线程之间互相竞争锁,提高了性能 if(instance == null) { synchronized(LazySingleton.class) { // 防止二次创建实例 if(instance == null) { instance = new LazySingleton(); } } } return instance; } }
内部类都是在第一次使用时才会被加载,而且静态内部类的加载不需要依附外部类,在使用时才加载。多个线程用到同一个类,而这个类还未被加载,则只有一个线程去加载类,其他线程等待。因此静态内部类模式的单例可以保证线程安全。
public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton(){} private static final Singleton getInstance() { return SingletonHolder.INSTANCE; } }
枚举天然解决了多线程同步执行的问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。
public enum Singleton { INSTANCE; public void method() {} }
下面是一些常见的具体应用场景:
日志记录器:在许多应用程序中,需要使用单例模式创建一个全局的日志记录器,以便记录系统的操作、错误和调试信息。单例模式确保只有一个日志记录器实例,并且可以在整个应用程序中使用。
数据库连接池:在具有多线程环境的应用程序中,使用单例模式创建一个数据库连接池可以提高性能和效率。单例模式可以确保只有一个连接池实例,并且多个线程可以共享这个实例来获取数据库连接。
配置信息管理器:在应用程序中,通常需要读取和管理配置信息,例如数据库连接参数、系统设置等。使用单例模式创建一个配置信息管理器可以确保只有一个实例来加载和保存配置信息,并且可以在整个应用程序***享。
线程池:在多线程环境中,线程池用于管理和控制线程的执行。使用单例模式创建一个全局的线程池可以避免多个线程池实例的创建和管理,从而提高线程的复用和性能。
缓存系统:在许多应用程序中,需要使用缓存来存储经常使用的数据,以提高访问速度。使用单例模式创建一个全局的缓存系统可以确保只有一个实例来管理缓存数据,并且可以在整个应用程序***享。
GUI应用程序中的窗口管理器:在GUI应用程序中,窗口管理器负责创建、管理和控制窗口的显示和交互。使用单例模式创建一个窗口管理器可以确保只有一个实例来管理窗口,并且可以在整个应用程序中统一管理。
第一种:饿汉模式(线程安全)
public class Single2 { private static Single2 instance = new Single2(); private Single2(){ System.out.println("Single2: " + System.nanoTime()); } public static Single2 getInstance(){ return instance; } }
第二种:懒汉模式 (如果方法没有synchronized,则线程不安全)
public class Single3 { private static Single3 instance = null; private Single3(){ System.out.println("Single3: " + System.nanoTime()); } public static synchronized Single3 getInstance(){ if(instance == null){ instance = new Single3(); } return instance; } }
第三种:懒汉模式改良版(线程安全,使用了double-check,即check-加锁-check,目的是为了减少同步的开销)
public class Single4 { private volatile static Single4 instance = null; private Single4(){ System.out.println("Single4: " + System.nanoTime()); } public static Single4 getInstance(){ if(instance == null){ synchronized (Single4.class) { if(instance == null){ instance = new Single4(); } } } return instance; } }
第四种:利用私有的内部工厂类(线程安全,内部类也可以换成内部接口,不过工厂类变量的作用于要改为public了。)
public class Singleton { private Singleton(){ System.out.println("Singleton: " + System.nanoTime()); } public static Singleton getInstance(){ return SingletonFactory.singletonInstance; } private static class SingletonFactory{ private static Singleton singletonInstance = new Singleton(); } }