在面试的时候面试官会怎么在单例模式中提问呢?你又该如何回答呢?可能你在面试的时候你会碰到这些问题:
那我们该怎么回答呢?那答案来了,看完接下来的内容就可以跟面试官唠唠单例模式了
单例模式是一种常用的软件设计模式,其属于创建型模式,其含义即是一个类只有一个实例,并为整个系统提供一个全局访问点 (向整个系统提供这个实)。
结构:
单例模式三要素:
在使用单例模式时,我们必须使用单例类提供的公有工厂方法得到单例对象,而不应该使用反射来创建,使用反射将会破坏单例模式 ,将会实例化一个新对象。
在单线程环境下,单例模式根据实例化对象时机的不同分为,
从速度和反应时间角度来讲,饿汉式(又称立即加载)要好一些;从资源利用效率上说,懒汉式(又称延迟加载)要好一些。
// 饿汉式单例 public class HungrySingleton{ // 私有静态实例引用,创建私有静态实例,并将引用所指向的实例 private static HungrySingleton singleton = new HungrySingleton(); // 私有的构造方法 private HungrySingleton(){} //返回静态实例的静态公有方法,静态工厂方法 public static HungrySingleton getSingleton(){ return singleton; } }
饿汉式单例,在类被加载时,就会实例化一个对象并将引用所指向的这个实例;更重要的是,由于这个类在整个生命周期中只会被加载一次,只会被创建一次,因此恶汉式单例是线程安全的。
因为类加载的方式是按需加载,且只加载一次。由于一个类在整个生命周期中只会被加载一次,在线程访问单例对象之前就已经创建好了,且仅此一个实例。即线程每次都只能也必定只可以拿到这个唯一的对象。
// 懒汉式单例 public class LazySingleton { // 私有静态实例引用 private static LazySingleton singleton; // 私有的构造方法 private LazySingleton(){} // 返回静态实例的静态公有方法,静态工厂方法 public static LazySingleton getSingleton(){ //当需要创建类的时候创建单例类,并将引用所指向的实例 if (singleton == null) { singleton = new LazySingleton(); } return singleton; } }
懒汉式单例是延迟加载,只有在需要使用的时候才会实例化一个对象,并将引用所指向的这个对象。
由于是需要时创建,在多线程环境是不安全的,可能会并发创建实例,出现多实例的情况,单例模式的初衷是相背离的。那我们需要怎么避免呢?可以看接下来的多线程中单例模式的实现形式。
非线程安全主要原因是,会有多个线程同时进入创建实例(if (singleton == null) {}代码块)的情况发生。当这种这种情形发生后,该单例类就会创建出多个实例,违背单例模式的初衷。因此,传统的懒汉式单例是非线程安全的。
在单线程环境下,无论是饿汉式单例还是懒汉式单例,它们都能够正常工作。但是,在多线程环境下就有可能发生变异:
那我们应该怎么在懒汉的基础上改造呢?
// 线程安全的懒汉式单例 public class SynchronizedSingleton { private static SynchronizedSingleton synchronizedSingleton; private SynchronizedSingleton(){} // 使用 synchronized 修饰,临界资源的同步互斥访问 public static synchronized SynchronizedSingleton getSingleton(){ if (synchronizedSingleton == null) { synchronizedSingleton = new SynchronizedSingleton(); } return synchronizedSingleton; } }
使用 synchronized 修饰 getSingleton()方法,将getSingleton()方法进行加锁,实现对临界资源的同步互斥访问,以此来保证单例。
虽然可现实线程安全,但由于同步的作用域偏大、锁的粒度有点粗,会导致运行效率会很低。
// 线程安全的懒汉式单例 public class BlockSingleton { private static BlockSingleton singleton; private BlockSingleton(){} public static BlockSingleton getSingleton2(){ synchronized(BlockSingleton.class){ // 使用 synchronized 块,临界资源的同步互斥访问 if (singleton == null) { singleton = new BlockSingleton(); } } return singleton; } }
其实synchronized块跟synchronized方法类似,效率都偏低。
// 线程安全的懒汉式单例 public class InsideSingleton { // 私有内部类,按需加载,用时加载,也就是延迟加载 private static class Holder { private static InsideSingleton insideSingleton = new InsideSingleton(); } private InsideSingleton() { } public static InsideSingleton getSingleton() { return Holder.insideSingleton; } }
使用双重检测同步延迟加载去创建单例,不但保证了单例,而且提高了程序运行效率。
// 线程安全的懒汉式单例 public class DoubleCheckSingleton { //使用volatile关键字防止重排序,因为 new Instance()是一个非原子操作,可能创建一个不完整的实例 private static volatile DoubleCheckSingleton singleton; private DoubleCheckSingleton() { } public static DoubleCheckSingleton getSingleton() { // Double-Check idiom if (singleton == null) { synchronized (DoubleCheckSingleton.class) { // 只需在第一次创建实例时才同步 if (singleton == null) { singleton = new DoubleCheckSingleton(); } } } return singleton; } }
为了在保证单例的前提下提高运行效率,我们需要对singleton实例进行第二次检查,为的式避开过多的同步(因为同步只需在第一次创建实例时才同步,一旦创建成功,以后获取实例时就不需要同步获取锁了)。
但需要注意的必须使用volatile关键字修饰单例引用,为什么呢?
如果没有使用volatile关键字是可能会导致指令重排序情况出现,在Singleton 构造函数体执行之前,变量 singleton可能提前成为非 null 的,即赋值语句在对象实例化之前调用,此时别的线程将得到的是一个不完整(未初始化)的对象,会导致系统崩溃。
此可能为程序执行步骤:
这种安全隐患正是由于指令重排序的问题所导致的。而volatile 关键字正好可以完美解决了这个问题。使用volatile关键字修饰单例引用就可以避免上述灾难。
NOTE
new 操作会进行三步走,预想中的执行步骤:
memory = allocate(); //1:分配对象的内存空间 ctorInstance(memory); //2:初始化对象 singleton = memory; //3:使singleton3指向刚分配的内存地址但实际上,这个过程可能发生无序写入(指令重排序),可能会导致所下执行步骤:
memory = allocate(); //1:分配对象的内存空间 singleton3 = memory; //3:使singleton3指向刚分配的内存地址 ctorInstance(memory); //2:初始化对象
借助于 ThreadLocal,我们可以实现双重检查模式的变体。我们将临界资源线程局部化,具体到本例就是将双重检测的第一层检测条件 if (instance == null) 转换为 线程局部范围内的操作 。
// 线程安全的懒汉式单例 public class ThreadLocalSingleton // ThreadLocal 线程局部变量 private static ThreadLocal<ThreadLocalSingleton> threadLocal = new ThreadLocal<ThreadLocalSingleton>(); private static ThreadLocalSingleton singleton = null; private ThreadLocalSingleton(){} public static ThreadLocalSingleton getSingleton(){ if (threadLocal.get() == null) { // 第一次检查:该线程是否第一次访问 createSingleton(); } return singleton; } public static void createSingleton(){ synchronized (ThreadLocalSingleton.class) { if (singleton == null) { // 第二次检查:该单例是否被创建 singleton = new ThreadLocalSingleton(); // 只执行一次 } } threadLocal.set(singleton); // 将单例放入当前线程的局部变量中 } }
借助于 ThreadLocal,我们也可以实现线程安全的懒汉式单例。但与直接双重检查模式使用,使用ThreadLocal的实现在效率上还不如双重检查锁定。
它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,
直接通过Singleton.INSTANCE.whateverMethod()的方式调用即可。方便、简洁又安全。
public enum EnumSingleton { instance; public void whateverMethod(){ //dosomething } }
使用多个线程,并使用hashCode值计算每个实例的值,值相同为同一实例,否则为不同实例。
public class Test { public static void main(String[] args) { Thread[] threads = new Thread[10]; for (int i = 0; i < threads.length; i++) { threads[i] = new TestThread(); } for (int i = 0; i < threads.length; i++) { threads[i].start(); } } } class TestThread extends Thread { @Override public void run() { // 对于不同单例模式的实现,只需更改相应的单例类名及其公有静态工厂方法名即可 int hash = Singleton5.getSingleton5().hashCode(); System.out.println(hash); } }
单例模式是 Java 中最简单,也是最基础,最常用的设计模式之一。在运行期间,保证某个类只创建一个实例,保证一个类仅有一个实例,并提供一个访问它的全局访问点 ,介绍单例模式的各种写法:
懒汉式单例
使用双重检查模式
各位看官还可以吗?喜欢的话,动动手指点个💗,点个关注呗!!谢谢支持!
欢迎扫码关注,原创技术文章第一时间推出