专栏首页Java识堂深入理解单例模式

深入理解单例模式

前言

有一些对象其实我们只需要一个,比方说:线程池,缓存,对话框,处理偏好设置和注册表的对象,日志对象,充当打印机,显卡等设备的驱动程序的对象。事实上,这类对象只能有一个实例,如果制造出多个实例,就会导致许多问题产生,例如:程序的行为异常,资源使用过量,或者是不一致的结果

涉及到一些类加载的知识,如果不清楚,可以看一下这篇分享:Java类的加载顺序

单例模式确保一个类只有一个实例,并提供一个全局访问点,实现单例模式的方法是私有化构造函数,通过getInstance()方法实例化对象,并返回这个实例

实现

第一种(懒汉)

按照上面的想法,我们有了第一个实现

// code1
public class Singleton {

    private static Singleton uniqueInstance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}

当2个线程同时进入getInstance()的if语句里面,会返回2个不同实例,因此这种方式是线程不安全的

// code2
public class Singleton {

    private static Singleton uniqueInstance;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}

用synchronized修饰可以保证线程安全,但是只有第一次执行此方法时才需要同步,设置好 uniqueInstance,就不需要同步这个方法了,之后每次调用这个方法,同步都是一种累赘

第二种(双重检查锁定)

synchronized锁的粒度太大,人们就想到通过双重检查锁定来降低同步的开销,下面是实例代码

// code3
public class Singleton {

    private static Singleton uniqueInstance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (uniqueInstance == null) {
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

如上面代码所示,如果第一次检查uniqueInstance不为null,那么就不需要执行下面的加锁和初始化操作,可以大幅降低synchronized带来的性能开销,只有在多个线程试图在同一时间创建对象时,会通过加锁来保证只有一个线程能创建对象

双重检查锁定看起来似乎很完美,但这是一个错误的优化!在线程执行到getInstance()方法的第4行,代码读取到uniqueInstance不为null时,uniqueInstance引用的对象有可能还没有完成初始化

简单概述一下《Java并发编程的艺术》的解释, uniqueInstance = new Singleton()可以分解为如下三行伪代码

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

3行伪代码中的2和3之间,可能会被重排序,重排序后执行时序如下

memory = allocate();    // 1:分配对象的内存空间
uniqueInstance = memory;// 3:设置uniqueInstance指向刚分配的内存地址
                        // 注意,此时对象还没有被初始化
ctorInstance(memory);   // 2:初始化对象

多个线程访问时可能出现如下情况

时间

线程A

线程B

t1

A1:分配对象的内存空间

t2

A3:设置uniqueinstance指向内存空间

t3

B1:判断uniqueinstance是否为空

t4

B2:由于uniqueinstace不为null,线程B间访问uniqueinstance引用的对象

t5

A2:初始化对象

t6

A4:访问instace引用的对象

这样会导致线程B访问到一个还未初始化的对象,此时可以用volatile来修饰Singleton,这样3行伪代码中的2和3之间的重排序,在多线程环境中将会被禁止

// code4
public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (uniqueInstance == null) {
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

第三种(饿汉)

如果应用程序总是创建并使用单例式例,或者在创建和运行时方面的负担不太繁重,我们可以以饿汉式的方式来创建单例

// code5
public class Singleton {

    private static Singleton uniqueInstance = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return uniqueInstance;
    }
}
// code6
public class Singleton {

    private static Singleton uniqueInstance;

    static {
        uniqueInstance = new Singleton();
    }

    private Singleton() {}

    public static Singleton getInstance() {
        return uniqueInstance;
    }
}

在类加载的时候直接创建这个对象,这样既能提高效率,又能保证线程安全,code5和code6几乎没有区别,因为静态成员变量和静态代码块都是类初始化的时候被加载

第四种(静态内部类)

// code7
public class Singleton {

    private static class SingletonHolder {
        private static Singleton uniqueInstance = new Singleton();
    }

    private Singleton() {}

    public static Singleton getInstance() {
        return SingletonHolder.uniqueInstance;
    }
}

饿汉式的方式只要Singleton类被装载了,那么uniqueInstance就会被实例化(没有达到lazy loading效果),而这种方式是Singleton类被装载了,uniqueInstance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化uniqueInstance

第五种(枚举)

// code8
public enum Singleton {  
   INSTANCE;  
   public void whateverMethod() {  
   }  
}

公认的实现单例的最好方式,网上资料也比较少,还没有彻底理解清楚,说不定以后会补一篇文章说明,本篇文章中的code1和code3不建议使用,原因已经说明

参考书籍

《Java并发编程的艺术》

《Head First设计模式》

本文分享自微信公众号 - Java识堂(erlieStar)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2018-06-16

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java 并发篇-04.synchronized

    大家好,缓缓来迟的第 5 篇 并发内容,其实准备了好久了,因为想写点不一样的内容,结果导致托了一个月才准备好。在开始正文之前,继续来我们的几个灵魂问题:

    haoming1100
  • 【趣学程序】Java中的异常

    趣学程序
  • 听说优秀的程序员20%的时间都在写UT?

    在今天的文章中打算和大家聊一聊关于测试的话题,也许有朋友会问,作为一名码农为什么要关注测试的问题?我们把代码开发完基本自测没问题了,扔给测试不就行了?有问题再改...

    用户5927304
  • java 并发篇- 概念篇

    各位好,今天是我们并发篇正式开始的第一篇,既然我们大家学习并发,那么就要理解一些计算机概念最好,否则,知道怎么用而不知道名称是啥,概念含糊不清,以及不知道怎么设...

    haoming1100
  • 为什么项目中用了JOOQ后大家都不愿再用Mybatis?

    今天给大家介绍一个新的ORM框架->JOOQ,可能很多朋友还没有听说过这个框架,码农哥之前也是一直在使用Mybatis框架作为Java工程中的持久层访问框架,但...

    用户5927304
  • 如何测试概率型业务接口

    在我做接口测试的工作中,有段时间负责产品的每星期活动的接口测试,需求一般比较简单,但是有一批活动很特殊,在之前的工作中都没遇到过,就是概率型业务。常见的抽奖、随...

    八音弦
  • 你知道 OOM 常见原因吗?有什么好的解决方法?

    当 JVM 内存严重不足时,就会抛出 java.lang.OutOfMemoryError 错误。本文总结了常见的 OOM 原因及其解决方法,如下图所示。如有遗...

    用户1516716
  • Java 并发篇03 -序性、可见性、原子性。

    这篇文章,我们将给大家来讲解引起我们并发问题的三大因素--— 有序性、可见性、原子性。这三个问题是属于并发领域的所以并不涉及语言。

    haoming1100
  • 上车时机已到--.NETCore是适应时代发展的雄鹰利剑

    随着 .NET Core 3.0-prevew7 的发布,开源社区的一支重要力量重要即将起飞;官方指出,该预览版为可能为最终版本,在很长一段时间内,.NETCo...

    梁规晓
  • 如何动手撸一个简单的LFU缓存

    关于第一种FIFO策略的实现,比较简单,可采用固定长度的数组和链表来处理,这里就不重点说了。今天我们的重点是LFU缓存的实现。

    我是攻城师

扫码关注云+社区

领取腾讯云代金券