面试官:说说单例模式!利用这个方法,我成功套路的面试官

简介: 前言在面试的时候面试官会怎么在单例模式中提问呢?你又该如何回答呢?可能你在面试的时候你会碰到这些问题:为什么说饿汉式单例天生就是线程安全的?传统的懒汉式单例为什么是非线程安全的?怎么修改传统的懒汉式单例,使其线程变得安全?线程安全的单例的实现还有哪些,怎么实现?双重检查模式、Volatile关键字 在单例模式中的应用ThreadLocal 在单例模式中的应用枚举式单例那我们该怎么回答呢?那答案来了,看完接下来的内容就可以跟面试官唠唠单例模式了

前言

分割线.jpg

在面试的时候面试官会怎么在单例模式中提问呢?你又该如何回答呢?可能你在面试的时候你会碰到这些问题:

为什么说饿汉式单例天生就是线程安全的?

传统的懒汉式单例为什么是非线程安全的?

怎么修改传统的懒汉式单例,使其线程变得安全?

线程安全的单例的实现还有哪些,怎么实现?

双重检查模式、Volatile关键字 在单例模式中的应用

ThreadLocal 在单例模式中的应用

枚举式单例

那我们该怎么回答呢?那答案来了,看完接下来的内容就可以跟面试官唠唠单例模式了


单例模式简介


单例模式是一种常用的软件设计模式,其属于创建型模式,其含义即是一个类只有一个实例,并为整个系统提供一个全局访问点 (向整个系统提供这个实)。

结构:


image.png

单例模式三要素:

私有的构造方法;

私有静态实例引用;

返回静态实例的静态公有方法。


单例模式的优点


在内存中只有一个对象,节省内存空间;

避免频繁的创建销毁对象,可以提高性能;

避免对共享资源的多重占用,简化访问;

为整个系统提供一个全局访问点。


单例模式的注意事项


在使用单例模式时,我们必须使用单例类提供的公有工厂方法得到单例对象,而不应该使用反射来创建,使用反射将会破坏单例模式 ,将会实例化一个新对象。


单线程实现方式


在单线程环境下,单例模式根据实例化对象时机的不同分为,

饿汉式单例(立即加载),饿汉式单例在单例类被加载时候,就实例化一个对象并将引用所指向的这个实例;

懒汉式单例(延迟加载),只有在需要使用的时候才会实例化一个对象将引用所指向的这个实例。

从速度和反应时间角度来讲,饿汉式(又称立即加载)要好一些;从资源利用效率上说,懒汉式(又称延迟加载)要好一些。


饿汉式单例


// 饿汉式单例
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) {}代码块)的情况发生。当这种这种情形发生后,该单例类就会创建出多个实例,违背单例模式的初衷。因此,传统的懒汉式单例是非线程安全的。


多线程实现方式


在单线程环境下,无论是饿汉式单例还是懒汉式单例,它们都能够正常工作。但是,在多线程环境下就有可能发生变异:

饿汉式单例天生就是线程安全的,可以直接用于多线程而不会出现问题

懒汉式单例本身是非线程安全的,因此就会出现多个实例的情况,与单例模式的初衷是相背离的。

那我们应该怎么在懒汉的基础上改造呢?

synchronized方法

synchronized块

使用内部类实现延迟加载


synchronized方法


// 线程安全的懒汉式单例
public class SynchronizedSingleton {privatestatic SynchronizedSingleton synchronizedSingleton;
privateSynchronizedSingleton(){}
// 使用 synchronized 修饰,临界资源的同步互斥访问    public static synchronized SynchronizedSingleton getSingleton(){        if (synchronizedSingleton == null) {            synchronizedSingleton = new SynchronizedSingleton();        }        return synchronizedSingleton;    }}

使用 synchronized 修饰 getSingleton()方法,将getSingleton()方法进行加锁,实现对临界资源的同步互斥访问,以此来保证单例。

虽然可现实线程安全,但由于同步的作用域偏大、锁的粒度有点粗,会导致运行效率会很低。


synchronized块


// 线程安全的懒汉式单例
public class BlockSingleton {privatestatic BlockSingleton singleton;
privateBlockSingleton(){}
public static BlockSingleton getSingleton2(){        synchronized(BlockSingleton.class){  // 使用 synchronized 块,临界资源的同步互斥访问            if (singleton == null) {                singleton = new BlockSingleton();            }        }        return singleton;    }}
 其实synchronized块跟synchronized方法类似,效率都偏低。


使用内部类实现延迟加载


// 线程安全的懒汉式单例
public class InsideSingleton {    // 私有内部类,按需加载,用时加载,也就是延迟加载privatestatic class Holder {
privatestatic InsideSingleton insideSingleton = new InsideSingleton();
}privateInsideSingleton() {
}    public static InsideSingleton getSingleton() {        return Holder.insideSingleton;    }}

如上述代码所示,我们可以使用内部类实现线程安全的懒汉式单例,这种方式也是一种效率比较高的做法。其跟饿汉式单例原理是相同的, 但可能还存在反射攻击或者反序列化攻击 。

双重检查(Double-Check idiom)现实

双重检查(Double-Check idiom)-volatile

使用双重检测同步延迟加载去创建单例,不但保证了单例,而且提高了程序运行效率。


//线程安全的懒汉式单例
publicclassDoubleCheckSingleton{
//使用volatile关键字防止重排序,因为newInstance()是一个非原子操作,可能创建一个不完整的实例
privatestaticvolatileDoubleCheckSingletonsingleton;
privateDoubleCheckSingleton(){
}
publicstaticDoubleCheckSingletongetSingleton(){
//Double-Checkidiom
if(singleton==null){
synchronized(DoubleCheckSingleton.class){
//只需在第一次创建实例时才同步
if(singleton==null){
singleton=newDoubleCheckSingleton();
}
}
}
returnsingleton;
}
}

为了在保证单例的前提下提高运行效率,我们需要对singleton实例进行第二次检查,为的是避开过多的同步(因为同步只需在第一次创建实例时才同步,一旦创建成功,以后获取实例时就不需要同步获取锁了)。

但需要注意的必须使用volatile关键字修饰单例引用,为什么呢?

如果没有使用volatile关键字是可能会导致指令重排序情况出现,在Singleton 构造函数体执行之前,变量 singleton可能提前成为非 null 的,即赋值语句在对象实例化之前调用,此时别的线程将得到的是一个不完整(未初始化)的对象,会导致系统崩溃。


此可能为程序执行步骤:


线程 1 进入 getSingleton() 方法,由于 singleton 为 null,线程 1 进入 synchronized 块 ;

同样由于 singleton为 null,线程 1 直接前进到 singleton = new DoubleCheckSingleton()处,在new对象的时候出现重排序,导致在构造函数执行之前,使实例成为非 null,并且该实例并未初始化的(原因在NOTE);

此时,线程 2 检查实例是否为 null。由于实例不为 null,线程 2 得到一个不完整(未初始化)的 Singleton 对象;

线程 1 通过运行 Singleton对象的构造函数来完成对该对象的初始化。

这种安全隐患正是由于指令重排序的问题所导致的。而volatile 关键字正好可以完美解决了这个问题。使用volatile关键字修饰单例引用就可以避免上述灾难。


NOTE


new 操作会进行三步走,预想中的执行步骤:

memory = allocate(); //1:分配对象的内存空间 ctorInstance(memory); //2:初始化对象 singleton = memory; //3:使singleton3指向刚分配的内存地址

但实际上,这个过程可能发生无序写入(指令重排序),可能会导致所下执行步骤:

memory = allocate(); //1:分配对象的内存空间 singleton3 = memory; //3:使singleton3指向刚分配的内存地址 ctorInstance(memory); //2:初始化对象


双重检查(Double-Check idiom)-ThreadLocal


借助于 ThreadLocal,我们可以实现双重检查模式的变体。我们将临界资源线程局部化,具体到本例就是将双重检测的第一层检测条件 if (instance == null) 转换为 线程局部范围内的操作 。


//线程安全的懒汉式单例
publicclassThreadLocalSingleton
//ThreadLocal线程局部变量
privatestaticThreadLocal<ThreadLocalSingleton>threadLocal=newThreadLocal<ThreadLocalSingleton>();
privatestaticThreadLocalSingletonsingleton=null;
privateThreadLocalSingleton(){}
publicstaticThreadLocalSingletongetSingleton(){
if(threadLocal.get()==null){//第一次检查:该线程是否第一次访问
createSingleton();
}
returnsingleton;
}
publicstaticvoidcreateSingleton(){
synchronized(ThreadLocalSingleton.class){
if(singleton==null){//第二次检查:该单例是否被创建
singleton=newThreadLocalSingleton();//只执行一次
}
}
threadLocal.set(singleton);//将单例放入当前线程的局部变量中
}
}

借助于 ThreadLocal,我们也可以实现线程安全的懒汉式单例。但与直接双重检查模式使用,使用ThreadLocal的实现在效率上还不如双重检查锁定。


枚举实现方式


它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,

直接通过Singleton.INSTANCE.whateverMethod()的方式调用即可。方便、简洁又安全。


publicenumEnumSingleton {
instance;publicvoidwhateverMethod(){
//dosomething
    }
}


测试单例线程安全性


使用多个线程,并使用hashCode值计算每个实例的值,值相同为同一实例,否则为不同实例。

publicclassTest{
publicstaticvoidmain(String[] args){
Thread[] threads =newThread[10];
for(inti =0; i < threads.length; i++) {
threads[i] =newTestThread();
}for(inti =0; i < threads.length; i++) {
threads[i].start();        }    }}classTestThreadextendsThread{
@Override
publicvoidrun(){
// 对于不同单例模式的实现,只需更改相应的单例类名及其公有静态工厂方法名即可
        int hash = Singleton5.getSingleton5().hashCode(); 
        System.out.println(hash);
    }
}


小结


单例模式是 Java 中最简单,也是最基础,最常用的设计模式之一。在运行期间,保证某个类只创建一个实例,保证一个类仅有一个实例,并提供一个访问它的全局访问点 ,介绍单例模式的各种写法:

饿汉式单例(线程安全)

懒汉式单例传统懒汉式单例(线程安全);使用synchronized方法实(线程安全);使用synchronized块实现懒汉式单例(线程安全);使用静态内部类实现懒汉式单例(线程安全)。

使用双重检查模式使用volatile关键字(线程安全);使用ThreadLocal实现懒汉式单例(线程安全)。

枚举式单例




相关文章
|
22天前
|
存储 Java 程序员
Java基础的灵魂——Object类方法详解(社招面试不踩坑)
本文介绍了Java中`Object`类的几个重要方法,包括`toString`、`equals`、`hashCode`、`finalize`、`clone`、`getClass`、`notify`和`wait`。这些方法是面试中的常考点,掌握它们有助于理解Java对象的行为和实现多线程编程。作者通过具体示例和应用场景,详细解析了每个方法的作用和重写技巧,帮助读者更好地应对面试和技术开发。
76 4
|
3月前
|
ARouter 测试技术 API
Android经典面试题之组件化原理、优缺点、实现方法?
本文介绍了组件化在Android开发中的应用,详细阐述了其原理、优缺点及实现方式,包括模块化、接口编程、依赖注入、路由机制等内容,并提供了具体代码示例。
48 2
|
4月前
|
Java
【Java基础面试二十】、介绍一下Object类中的方法
这篇文章介绍了Java中Object类的常用方法,包括`getClass()`、`equals()`、`hashCode()`、`toString()`、`wait()`、`notify()`、`notifyAll()`和`clone()`,并提到了不推荐使用的`finalize()`方法。
【Java基础面试二十】、介绍一下Object类中的方法
|
4月前
|
Java API 索引
【Java基础面试二十四】、String类有哪些方法?
这篇文章列举了Java中String类的常用方法,如`charAt()`、`substring()`、`split()`、`trim()`、`indexOf()`、`lastIndexOf()`、`startsWith()`、`endsWith()`、`toUpperCase()`、`toLowerCase()`、`replaceFirst()`和`replaceAll()`,并建议面试时展示对这些方法的熟悉度,同时深入理解部分方法的源码实现。
【Java基础面试二十四】、String类有哪些方法?
|
4月前
|
Java
【Java集合类面试三十】、BlockingQueue中有哪些方法,为什么这样设计?
BlockingQueue设计了四组不同行为方式的方法用于插入、移除和检查元素,以适应不同的业务场景,包括抛异常、返回特定值、阻塞等待和超时等待,以实现高效的线程间通信。
|
4月前
|
机器学习/深度学习 算法 Python
【机器学习】面试问答:决策树如何进行剪枝?剪枝的方法有哪些?
文章讨论了决策树的剪枝技术,包括预剪枝和后剪枝的概念、方法以及各自的优缺点。
59 2
|
4月前
|
SQL 安全 测试技术
[go 面试] 接口测试的方法与技巧
[go 面试] 接口测试的方法与技巧
|
4月前
|
机器学习/深度学习
【机器学习】面试题:LSTM长短期记忆网络的理解?LSTM是怎么解决梯度消失的问题的?还有哪些其它的解决梯度消失或梯度爆炸的方法?
长短时记忆网络(LSTM)的基本概念、解决梯度消失问题的机制,以及介绍了包括梯度裁剪、改变激活函数、残差结构和Batch Normalization在内的其他方法来解决梯度消失或梯度爆炸问题。
165 2
|
4月前
|
存储 机器学习/深度学习 缓存
【数据挖掘】XGBoost面试题:与GBDT的区别?为什么使用泰勒二阶展开?为什么可以并行训练?为什么快?防止过拟合的方法?如何处理缺失值?
XGBoost与GBDT的区别、XGBoost使用泰勒二阶展开的原因、并行训练的原理、速度优势、防止过拟合的策略以及处理缺失值的方法,突出了XGBoost在提升模型性能和训练效率方面的一系列优化。
157 1
|
4月前
|
机器学习/深度学习