8种单例模式写法助你搞定面试

1. 单例模式常见问题

为什么要有单例模式

单例模式是一种设计模式,它限制了实例化一个对象的行为,始终至多只有一个实例。当只需要一个对象来协调整个系统的操作时,这种模式就非常有用.它描述了如何解决重复出现的设计问题,
比如我们项目中的配置工具类,日志工具类等等。

如何设计单例模式 ?

1.单例类如何控制其实例化

2.如何确保只有一个实例

通过一下措施解决这些问题:

private构造函数,类的实例话不对外开放,由自己内部来完成这个操作,确保永远不会从类外部实例化类,避免外部随意new出来新的实例。

该实例通常存储为私有静态变量,提供一个静态方法,返回对实例的引用。如果是在多线程环境下则用锁或者内部类来解决线程安全性问题。

2. 单例类有哪些特点 ?

私有构造函数
它将阻止从类外部实例化新对象

它应该只有一个实例
这是通过在类中提供实例来方法完成的,阻止外部类或子类来创建实例。这是通过在java中使构造函数私有来完成的,这样任何类都不能访问构造函数,因此无法实例化它。

单实例应该是全局可访问的
单例类的实例应该是全局可访问的,以便每个类都可以使用它。在Java中,它是通过使实例的访问说明符为public来完成的。

节省内存,减少GC

因为是全局至多只有一个实例,避免了到处new对象,造成浪费内存,以及GC,有了单例模式可以避免这些问题

3. 单例模式8种写法

下面由我给大家介绍8种单例模式的写法,各有千秋,存在即合理,通过自己的使用场景选一款使用即可。我们选择单例模式时的挑选标准或者说评估一种单例模式写法的优劣时通常会根据一下两种因素来衡量:

1.在多线程环境下行为是否线程安全

2.饿汉以及懒汉

3.编码是否优雅(理解起来是否比较直观)

1. 饿汉式线程安全的

public class SingleTon{

    private static final SingleTon INSTANCE = new SingleTon();

    private SingleTon(){ }

    public static SingleTon getInstance(){
        return INSTANCE;
    }
    public static void main(String[] args) {
        SingleTon instance1 = SingleTon.getInstance();
        SingleTon instance2 = SingleTon.getInstance();
        System.out.println(instance1 == instance2);
    }

}

这种写法是非常简单实用的,值得推荐,唯一缺点就是懒汉式的,也就是说不管是否需要用到这个方法,当类加载的时候都会生成一个对象。
除此之外,这种写法是线程安全的。类加载到内存后,就实例化一个单例,JVM保证线程安全,

2. 饿汉式线程安全(变种写法)

public class SingleTon{

    private static final SingleTon INSTANCE ;

    static {
        INSTANCE = new SingleTon();
    }

    private SingleTon(){}

    public static SingleTon getInstance(){
        return INSTANCE;
    }
        public static void main(String[] args) {
        SingleTon instance1 = SingleTon.getInstance();
        SingleTon instance2 = SingleTon.getInstance();
        System.out.println(instance1 == instance2);
    }

}

3. 懒汉式线程不安全

public class SingleTon{

    private static  SingleTon instance ;

    private SingleTon(){}

    public static SingleTon getInstance(){
            if(instance == null){
                instance = new SingleTon();
            }
            return instance;
    }

    public static void main(String[] args) {
        SingleTon instance1 = SingleTon.getInstance();
        SingleTon instance2 = SingleTon.getInstance();
        System.out.println(instance1 == instance2);

        // 通过开启100个线程 比较是否是相同对象
        for(int i=0;i<100;i++){
             new Thread(()->
                System.out.println(SingleTon.getInstance().hashCode())
            ).start();
        }

    }

}

这种写法虽然达到了按需初始化的目的,但却带来线程不安全的问题,至于为什么在并发情况下上述的例子是不安全的呢 ?

   // 通过开启100个线程 比较是否是相同对象
        for(int i=0;i<100;i++){
             new Thread(()->
                System.out.println(SingleTon.getInstance().hashCode())
            ).start();
        }

为了使效果更直观一点我们对getInstance 方法稍做修改,每个线程进入之后休眠一毫秒,这样做的目的是为了每个线程都尽可能获得cpu时间片去执行。代码如下
public static SingleTon getInstance(){ if(instance == null){ try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } instance = new SingleTon(); } return instance; }

执行结果如下

上述的单例写法,我们是可以创造出多个实例的,至于为什么在这里要稍微解释一下,这里涉及了同步问题

造成线程不安全的原因:

当并发访问的时候,第一个调用getInstance方法的线程t1,在判断完singleton是null的时候,线程A就进入了if块准备创造实例,但是同时另外一个线程B在线程A还未创造出实例之前,就又进行了singleton是否为null的判断,这时singleton依然为null,所以线程B也会进入if块去创造实例,这时问题就出来了,有两个线程都进入了if块去创造实例,结果就造成单例模式并非单例。
注:这里通过休眠一毫秒来模拟线程挂起,为初始化完instance

为了解决这个问题,我们可以采取加锁措施,所以有了下面这种写法

4. 懒汉式线程安全(粗粒度Synchronized)

public class SingleTon{

    private static  SingleTon instance ;

    private SingleTon(){}

    public static SingleTon synchronized getInstance(){
        if(instance == null){
                instance = new SingleTon();
        }
        return instance;
    }

    public static void main(String[] args) {
        SingleTon instance1 = SingleTon.getInstance();
        SingleTon instance2 = SingleTon.getInstance();
        System.out.println(instance1 == instance2);
            // 通过开启100个线程 比较是否是相同对象
            for(int i=0;i<100;i++){
                new Thread(()->
                System.out.println(SingleTon.getInstance().hashCode())
            ).start();
        }

    }

}

由于第三种方式出现了线程不安全的问题,所以对getInstance方法加了synchronized来保证多线程环境下的线程安全性问题,这种做法虽解决了多线程问题但是效率比较低。
因为锁住了整个方法,其他进入的现成都只能阻塞等待了,这样会造成很多无谓的等待。

于是可能有人会想到可不可以让锁的粒度更细一点,只锁住相关代码块可否?所以有了第五种写法。

5. 懒汉式线程不安全(synchronized代码块)

public class SingleTon{

    private static  SingleTon instance ;

    private SingleTon(){}

    public static SingleTon getInstance(){
        if(insatnce == null){
            synchronied(SingleTon.class){
                    instance = new SingleTon();
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        SingleTon instance1 = SingleTon.getInstance();
        SingleTon instance2 = SingleTon.getInstance();
        System.out.println(instance1 == instance2);

        // 通过开启100个线程 比较是否是相同对象
        for(int i=0;i<100;i++){
             new Thread(()->
                System.out.println(SingleTon.getInstance().hashCode())
            ).start();
        }

    }

}

当并发访问的时候,第一个调用getInstance方法的线程t1,在判断完instance是null的时候,线程A就进入了if块并且持有了synchronized锁,但是同时另外一个线程t2在线程t1还未创造出实例之前,就又进行了instance是否为null的判断,这时instance依然为null,所以线程t2也会进入if块去创造实例,他会在synchronized代码外面阻塞等待,直到t1释放锁,这时问题就出来了,有两个线程都实例化了新的对象。

造成这个问题的原因就是线程进入了if块并且在等待synchronized锁的过程中有可能上一个线程已经创建了实例,所以进入synchronized代码块之后还需要在判断一次,于是有了下面这种双重检验锁的写法。

6. 懒汉式线程安全(双重检验加锁)

public class SingleTon{

    private static  volatile SingleTon instance ;

    private SingleTon(){}

    public static SingleTon getInstance(){
        if(instance == null){
            synchronied(SingleTon.class){
                    if(instance == null){
                        instance = new SingleTon();
                    }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        SingleTon instance1 = SingleTon.getInstance();
        SingleTon instance2 = SingleTon.getInstance();
        System.out.println(instance1 == instance2);

        // 通过开启100个线程 比较是否是相同对象
        for(int i=0;i<100;i++){
             new Thread(()->
                System.out.println(SingleTon.getInstance().hashCode())
            ).start();
        }

    }

}

这种写法基本趋于完美了,但是可能需要对一下几点需要进行解释:

  • 第一个判空(外层)的作用 ?
  • 第二个判空(内层)的作用 ?
  • 为什么变量修饰为volatile ?

第一个判空(外层)的作用

首先,思考一下可不可以去掉最外层的判断? 答案是:可以

其实仔细观察之后会发现最外层的判断跟能否线程安全正确生成单例无关!!!

它的作用是避免每次进来都要加锁或者等待锁,有了同步代码块之外的判断之后省了很多事,当我们的单例类实例化一个单例之后其他后续的所有请求都没必要在进入同步代码块继续往下执行了,直接返回我们曾生成的实例即可,也就是实例还未创建时才进行同步,否则就直接返回,这样就节省了很多无谓的线程等待时间,所以最外的判断可以认为是对提升性能有帮助。

第二个判空(内层)的作用

假设我们去掉同步块中的是否为null的判断,有这样一种情况,A线程和B线程都在同步块外面判断了instance为null,结果t1线程首先获得了线程锁,进入了同步块,然后t1线程会创造一个实例,此时instance已经被赋予了实例,t1线程退出同步块,直接返回了第一个创造的实例,此时t2线程获得线程锁,也进入同步块,此时t1线程其实已经创造好了实例,t2线程正常情况应该直接返回的,但是因为同步块里没有判断是否为null,直接就是一条创建实例的语句,所以t2线程也会创造一个实例返回,此时就造成创造了多个实例的情况。

为什么变量修饰为volatile

因为虚拟机在执行创建实例的这一步操作的时候,其实是分了好几步去进行的,也就是说创建一个新的对象并非是原子性操作。在有些JVM中上述做法是没有问题的,但是有些情况下是会造成莫名的错误。

首先要明白在JVM创建新的对象时,主要要经过三步。

1.分配内存

2.初始化构造器

3.将对象指向分配的内存的地址

因为仅仅一个new 新实例的操作就涉及三个子操作,所以生成对象的操作不是原子操作

而实际情况是,JVM会对以上三个指令进行调优,其中有一项就是调整指令的执行顺序(该操作由JIT编译器来完成)。

所以,在指令被排序的情况下可能会出现问题,假如 2和3的步骤是相反的,先将分配好的内存地址指给instance,然后再进行初始化构造器,这时候后面的线程去请求getInstance方法时,会认为instance对象已经实例化了,直接返回一个引用。

如果这时还没进行构造器初始化并且这个线程使用了instance的话,则会出现线程会指向一个未初始化构造器的对象现象,从而发生错误。

7. 静态内部类的方式(基本完美了)

public class SingleTon{

    public static SingleTon getInstance(){
        return StaticSingleTon.instance;
    }
    private static class StaticSingleTon{
            private static final SingleTon instance = new SingleTon();
    }
    public static void main(String[] args) {
        SingleTon instance1 = SingleTon.getInstance();
        SingleTon instance2 = SingleTon.getInstance();
        System.out.println(instance1 == instance2);

        // 通过开启100个线程 比较是否是相同对象
        for(int i=0;i<100;i++){
             new Thread(()->
                System.out.println(SingleTon.getInstance().hashCode())
            ).start();
        }

    }

}
  • 因为一个类的静态属性只会在第一次加载类时初始化,这是JVM帮我们保证的,所以我们无需担心并发访问的问题。所以在初始化进行一半的时候,别的线程是无法使用的,因为JVM会帮我们强行同步这个过程。
  • 另外由于静态变量只初始化一次,所以singleton仍然是单例的。

8. 枚举类型的单例模式(太完美以至于。。。)

public Enum SingleTon{

    INSTANCE;
    public static void main(String[] args) {
         // 通过开启100个线程 比较是否是相同对象
        for(int i=0;i<100;i++){
            new Thread(()->
                System.out.println(SingleTon.getInstance().hashCode())
            ).start();
        }

    }

}

这种写法从语法上看来是完美的,他解决了上面7种写法都有的问题,就是我们可以通过反射可以生成新的实例
但是枚举的这种写法是无法通过反射来生成新的实例,因为枚举没有public构造方法

原文地址:https://www.cnblogs.com/wyc1994666/p/11394755.html

时间: 2024-10-07 11:14:24

8种单例模式写法助你搞定面试的相关文章

企业邮箱选购妙招,3分钟助你搞定专业企业邮箱

企业邮箱作为公司商务往来,内部协同工作的重要桥梁,看似习以为常却蕴含着重要的影响.诸如部门的工作审批.重要的工作交接.与客户的签约合同等,均少不了它的存在.那么在选购企业邮箱时,如何不被当小白,快速搞定称心如意的企业邮箱呢? 一. 企业邮箱选购必知的几点属性 包括容量.附件.企业管理功能.硬件系统安全等,目前市面上邮箱官方报价都大同小异,两三百左右一个账号,一般为5个起卖,据了解,目前仅有TOM企邮支持5个以下的企邮申请.容量方面TOM企业邮箱也是率先提出了无限容量的功能优势 二. 企业邮箱选购

面试大总结之一:Java搞定面试中的链表题目

链表是面试中常考的,本文参考了其它一些文章,加上小编的自己总结,基本每个算法都测试并优化过. 算法大全(1)单链表 中还有一些链表题目,将来也会整理进来. * REFS: * http://blog.csdn.net/fightforyourdream/article/details/16353519 * http://blog.csdn.net/luckyxiaoqiang/article/details/7393134 轻松搞定面试中的链表题目 * http://www.cnblogs.co

常见的几种单例模式写法

单例模式:是一种常用的软件设计模式,在它的核心结构中值包含一个被称为单例的特殊类.一个类只有一个实例,即一个类只有一个对象实例. 对于系统中的某些类来说,只有一个实例很重要,例如,一个系统中可以存在多个打印任务,但是只能有一个正在工作的任务:售票时,一共有100张票,可有有多个窗口同时售票,但需要保证不要超售(这里的票数余量就是单例,售票涉及到多线程).如果不是用机制对窗口对象进行唯一化将弹出多个窗口,如果这些窗口显示的都是相同的内容,重复创建就会浪费资源. 应用场景(来源:<大话设计模式>)

怎样在表格中输入分数?这三种方法快速教你搞定!

很多对电脑不太熟悉的小伙伴,有些技巧是不知道怎样操作的.就拿在表格中输入分数吧,很多的小伙伴是不知道键盘上的哪个符号是分数的符号.今天小编给大家分享两种在表格中输入分数的技巧!No.1日期分数输入法1.我们先打开一下电脑中的表格,然后在表格中输入一个分数,看一下会不会变成日期的形式.确实变成日期了.右键单击选择[设置单元格格式],我们可以看到日期默认的是自定义的单元格格式.2.然后我们点击日期,选择一个以分数表示的日期格式,在日期的类型中选择中3/14的日期格式就可以了.No.2单元格格式分数输

【搞定面试官】谈谈你对JDK中Executor的理解?

前言 随着当今处理器计算能力愈发强大,可用的核心数量越来越多,各个应用对其实现更高吞吐量的需求的不断增长,多线程 API 变得非常流行.在此背景下,Java自JDK1.5 提供了自己的多线程框架,称为 Executor 框架. 1. Executor 框架是什么? 1.1 简介 Java Doc中是这么描述的 An object that executes submitted Runnable tasks. This interface provides a way of decoupling

【搞定面试官】你还在用Executors来创建线程池?会有什么问题呢?

前言 上文我们介绍了JDK中的线程池框架Executor.我们知道,只要需要创建线程的情况下,即使是在单线程模式下,我们也要尽量使用Executor.即: ExecutorService fixedThreadPool = Executors.newFixedThreadPool(1); //此处不该利用Executors工具类来初始化线程池 但是,在<阿里巴巴Java开发手册>中有一条 [强制]线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方

【免费IT求职公开课】一个月搞定面试算法!第一节免费试听!

第一节免费试听时间] 北京时间 2015-7-19 09:30 (周日a.m.) 美西时间 2015-7-18 18:30 (周六) 免费试听报名网址 http://www.jiuzhang.com/course/1/ 本课程为网络直播课,报名试听后,即可收到听课链接. 请在课程时间内访问该链接,即可参与试听. 课程特色 1. 一流的师资,硅谷顶尖IT企业工程师在线授课,讲师均有ACM/world final背景. 2. 由0到1.由易到难,适合算法基础相对薄弱的 or 转专业的 or 想跳槽却

搞定面试算法系列 —— 分治算法三步走

主要思想 分治算法,即分而治之:把一个复杂问题分成两个或更多的相同或相似子问题,直到最后子问题可以简单地直接求解,最后将子问题的解合并为原问题的解. 归并排序就是一个典型的分治算法. 三步走 和把大象塞进冰箱一样,分治算法只要遵循三个步骤即可:分解 -> 解决 -> 合并. 分解:分解原问题为结构相同的子问题(即寻找子问题) 解决:当分解到容易求解的边界后,进行递归求解 合并:将子问题的解合并成原问题的解 这么一说似乎还是有点抽象?那我们通过经典的排序算法归并排序来体验一下分治算法的核心思想.

两种单例模式的写法

iOS的单例模式有两种官方写法,如下: (1)不使用GCD #import "ServiceManager.h" static ServiceManager *defaultManager; @implementation ServiceManager +(ServiceManager *)defaultManager{ if(!defaultManager) defaultManager=[[self allocWithZone:NULL] init]; return default