热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

前言 在面试的时候面试官会怎么在单例模式中提问呢?你又该如何回答呢?可能你在面试的时候你会碰到这些问题: 为什么说饿汉式单例天生就是线程安全的? 传统的懒汉式单例为什么是非线程安全的? 怎么修改

前言

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

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

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

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

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

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

ThreadLocal 在单例模式中的应用

枚举式单例

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

单例模式简介

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

结构:

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

单例模式三要素:

私有的构造方法;

私有静态实例引用;

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

单例模式的优点

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

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

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

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

单例模式的注意事项

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

单线程实现方式

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

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

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

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

饿汉式单例

// 饿汉式单例

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线程局部变量

privatestaticThreadLocalthreadLocal=newThreadLocal();

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[i] =newTestThread();

}for(inti =0; i

threads[i].start();        }    }}classTestThreadextendsThread{

@Override

publicvoidrun(){

// 对于不同单例模式的实现,只需更改相应的单例类名及其公有静态工厂方法名即可

        int hash = Singleton5.getSingleton5().hashCode(); 

        System.out.println(hash);

    }

}

小结

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

饿汉式单例(线程安全)

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

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

枚举式单例

各位看官还可以吗?喜欢的话,动动手指点个,点个关注呗!!谢谢支持!


推荐阅读
  • 本文介绍了Java高并发程序设计中线程安全的概念与synchronized关键字的使用。通过一个计数器的例子,演示了多线程同时对变量进行累加操作时可能出现的问题。最终值会小于预期的原因是因为两个线程同时对变量进行写入时,其中一个线程的结果会覆盖另一个线程的结果。为了解决这个问题,可以使用synchronized关键字来保证线程安全。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • 本文整理了Java面试中常见的问题及相关概念的解析,包括HashMap中为什么重写equals还要重写hashcode、map的分类和常见情况、final关键字的用法、Synchronized和lock的区别、volatile的介绍、Syncronized锁的作用、构造函数和构造函数重载的概念、方法覆盖和方法重载的区别、反射获取和设置对象私有字段的值的方法、通过反射创建对象的方式以及内部类的详解。 ... [详细]
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • 猜字母游戏
    猜字母游戏猜字母游戏——设计数据结构猜字母游戏——设计程序结构猜字母游戏——实现字母生成方法猜字母游戏——实现字母检测方法猜字母游戏——实现主方法1猜字母游戏——设计数据结构1.1 ... [详细]
  • Android源码深入理解JNI技术的概述和应用
    本文介绍了Android源码中的JNI技术,包括概述和应用。JNI是Java Native Interface的缩写,是一种技术,可以实现Java程序调用Native语言写的函数,以及Native程序调用Java层的函数。在Android平台上,JNI充当了连接Java世界和Native世界的桥梁。本文通过分析Android源码中的相关文件和位置,深入探讨了JNI技术在Android开发中的重要性和应用场景。 ... [详细]
author-avatar
45度向上倾斜的世界取_872
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有