Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >单例模式

单例模式

作者头像
栋先生
发布于 2018-09-29 08:46:41
发布于 2018-09-29 08:46:41
48500
代码可运行
举报
文章被收录于专栏:Java成长之路Java成长之路
运行总次数:0
代码可运行

单例模式是常见的一种设计模式,本文就来总结一下单例模式的几种写法。

一、饿汉式

所谓饿汉式单例设计模式,就是将类的静态实例作为该类的一个成员变量,也就是说在JVM 加载它的时候就已经创建了该类的实例,因此它不会存在多线程的安全问题。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 饿汉式版单例模式的实现
 */
public class Singleton {
    public static final Singleton instance = new Singleton();

    public Singleton() {
        //empty
    }

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

此种写法唯一的缺点就是:不支持懒加载(lazy initialization),存在两个缺点。

  • 缺点一:饿汉式提前对实例进行了初始化或者说构造,假设构造该类需要很多的性能消耗,如果代码写成这个样子将会提前完成构造,又假设我们在系统运行过程中压根就没有对该实例进行使用,那岂不是很浪费系统的资源呢?
  • 缺点二:譬如 Singleton 实例的创建是依赖参数或者配置文件的,在 getInstance() 之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。

二、懒汉式

所谓懒汉式单例模式的意思就是,实例虽然作为该类的一个实例变量,但是它不主动进行创建,如果你不使用它那么它将会永远不被创建,只有你在第一次使用它的时候才会被创建,并且得到保持。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 懒汉式版单例模式的实现
 */
public class Singleton {
    public static Singleton instance;

    public Singleton() {
        //empty
    }

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

上述代码就是传统懒汉式实现的方式,如果我们对于多线程有一点了解,会发现上述代码存在着线程安全问题。在多线程的情况下,instance实例存在着可能被创建多次的情况。下面就来分析一下出现线程安全问题的情况:

懒汉式出现线程安全问题

假设有①、②两个线程同时在获取这个instance,则有可能出现如上图中所示的这种情况。也就是当①线程执行完了null == instance但未创建Instance实例时,恰巧cpu执行时间到了,①线程让出了cpu的执行权。于是②线程也进入到了null == instance中,判断到了instance没有被创建,因此分别实例化了一个以上的Instance。 这样的单例类是很危险的,那么我们应该如何避免多线程引起的问题呢?请看下面的单例模式的实现代码。

三、同步方法版懒汉式

为了解决上面线程同步的问题, 最简单的方法是将整个getInstance()方法设为同步(synchronized)。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 同步方法版懒汉式,使用synchronized在getInstance方法上加锁
 */
public class Singleton {
    public static Singleton instance;

    public Singleton() {
        //empty
    }

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

但是该方法的效率将是相当低下的,因为每一次调用都要获取锁,判断锁的状态,使得getInstance()方法完全变成了一个串行化的方法。因此就会出现解决了安全问题,带来了很大的效率问题。我们怎么能在解决线程安全问题的同时,尽可能的提高程序的效率呢?请看下面的代码。

四、双重校验锁

双重检验锁模式(double checked locking pattern),是一种使用同步块加锁的方法。程序员称其为双重检查锁,因为会有两次检查 instance == null,一次是在同步块外,一次是在同步块内。代码如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class Singleton {
    private static Singleton instance;

    private Singleton() {
        //---
    }

    //double check
    public static Singleton getInstance() {

        if (null == instance) {
            synchronized (Singleton.class) {
                if (null == instance)
                    instance = new Singleton();
            }
        }

        return Singleton.instance;
    }
}

让我们分析一下,上述双重校验锁模式是如何将效率的损耗降到最低的。

如上图所示,当①线程进入到如图所示位置,判断Instancenull,并且初始化了Instance;②线程进入到如图所示位置,判断null==instance条件不成立,直接退出;当③线程进入到了如图所示位置发现null==instance不成立,则直接返回。 通过上述代码的分析,我们可以发现,锁的等待或者争抢最多发生两次,也就是同步代码块中的代码最多被执行两次,如此一来,安全问题解决了,效率问题也被解决掉了。

这段代码看起来很完美,很可惜,它是有问题, 可能会出现空指针异常。主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情(具体可以参考:JVM之对象的创建)。

  1. 给 instance 分配内存
  2. 调用 Singleton 的构造函数来初始化成员变量
  3. 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance。如果我们在这个时候使用instead,就会顺理成章地报错。 那么怎么解决这种情况呢,我们只需要将 instance 变量声明成 volatile 就可以了!

4.1 volatile版双重校验锁

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class Singleton {

    private static volatile Singleton instance;

    private Singleton() {
        //
    }

    //double check add volatile
    public static Singleton getInstance() {

        if (null == instance) {
            synchronized (SingletonObject4.class) {
                if (null == instance)
                    instance = new Singleton();
            }
        }
        return Singleton.instance;
    }
}

有些人认为使用 volatile 的原因是 可见性,也就是可以保证线程在本地不会存有 instance 的副本,每次都是去主内存中读取。但其实是不对的。使用 volatile 的主要原因是其另一个特性:禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。从「先行发生原则」的角度理解的话,就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作(这里的“后面”是时间上的先后顺序)。

五、静态内部类版单例模式

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class Singleton {

    private Singleton() {

    }

    private static class InstanceHolder {
        private final static Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
        return InstanceHolder.instance;
    }
}

这种方式比较完美,它具有以下的优点: 1. 使用JVM本身机制保证了线程安全的问题; 2. 由于InstanceHolder是私有的,除了getInstance()之外没有办法访问它,因此它是懒汉式; 3. 读取实例的时候不会进行同步,可以保证线程安全。不需要加锁,没有性能缺陷。也不依赖 JDK 版本。

六、枚举Enum版单例模式

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class Singleton {
    private Singleton() {

    }

    private enum Singleton {
        INSTANCE;

        private final Singleton instance;

        Singleton() {
            instance = new Singleton();
        }

        public Singleton getInstance() {
            return instance;
        }
    }

    public static Singleton getInstance() {
        return Singleton.INSTANCE.getInstance();
    }

    public static void main(String[] args) {
        IntStream.rangeClosed(1, 100)
                .forEach(i -> new Thread(String.valueOf(i)) {
                    @Override
                    public void run() {
                        System.out.println(Singleton.getInstance());
                    }
                }.start());
    }
}

我们可以通过Singleton.INSTANCE.getInstance()来访问实例,这比调用getInstance()方法简单多了。创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象。但是还是很少看到有人这样写,可能是因为不太熟悉吧。

总结

一般来说,单例模式有六种写法:懒汉、饿汉、同步方法、双重检验锁、静态内部类、枚举。上述所说都是线程安全的实现,文章开头给出的第一种方法不算正确的写法。

就我个人而言,一般情况下直接使用饿汉式就好了,如果明确要求要懒加载(lazy initialization)会倾向于使用静态内部类,如果涉及到反序列化创建对象时会试着使用枚举的方式来实现单例。

参考文章: 如何正确地写出单例模式

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018年03月12日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
灵活多变的单例模式
在软件工程领域,设计模式是一套通用、可复用的解决方案,用于解决在软件设计过程中产生的通用问题。它不是一个可以直接转成源码的设计,是一套开发人员在软件设计过程中应当遵循的规范。也就是说没有设计模式,软件依旧可以开发,只是后期维护可能变得不那么轻松。设计模式就是为了简化你的维护成本提升性能而设计的,不同的设计模式适用场景各异,具体的结合实际场景对待。
啃饼思录
2021/11/02
3270
JAVA设计模式之单例模式
java中单例模式是一种常见的设计模式,单例模式的写法有好几种,这里主要介绍三种:懒汉式单例、饿汉式单例。
秋白
2019/07/02
4230
“人尽皆知”的单例模式
单例模式(Singleton),目的是为了保证在一个进程中,某个类有且仅有一个实例。
架构狂人
2023/10/04
2520
“人尽皆知”的单例模式
设计模式---单例模式
当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的
大忽悠爱学习
2021/11/15
2210
深入理解单例模式
Java面试通关手册(Java学习指南,欢迎Star,会一直完善下去,欢迎建议和指导):https://github.com/Snailclimb/Java_Guide
用户2164320
2018/06/14
6090
Java 单例模式通俗说
定义:单例模式就是将类的构造函数进行private化,然后只留出一个静态的Instance函数供外部调用者调用。
sowhat1412
2020/11/05
5080
单例模式的8种写法
JVM类加载过程中,虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行<clinit>()方法后,其他线程唤醒之后不会再次进入<clinit>()方法。同一个加载器下,一个类型只会初始化一次。),在实际应用中,这种阻塞往往是很隐蔽的。
有一只柴犬
2024/01/25
1370
单例模式的8种写法
单例模式 Java 简介 学习笔记 及多种实现方式
在我们的系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、注册表、日志对象、充当打印机、显卡等设备驱动程序的对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生,比如:程序的行为异常、资源使用过量、或者不一致性的结果。
大鹅
2020/06/24
9850
设计模式之单例模式实践
概念 单例模式即一个JVM内存中只存在一个类的对象实例 分类 1、懒汉式 类加载的时候就创建实例 2、饿汉式 使用的时候才创建实例 当然还有其他的生成单例的方式,双重校验锁,枚举和静态内部类,文中会有
Java技术栈
2018/03/29
6570
单例模式的七种写法,你都知道吗?
因为instance是个静态变量,所以它会在类加载的时候完成实例化,不存在线程安全的问题。
三分恶
2021/09/14
4700
为什么用枚举类来实现单例模式越来越流行?
这是设计模式的第一篇文章,我们从单例模式开始入手,单例模式是 Java 设计模式中最简单的一种,只需要一个类就能实现单例模式,但是,你可不能小看单例模式,虽然从设计上来说它比较简单,但是在实现当中你会遇到非常多的坑,所以,系好安全带,上车。
Bug开发工程师
2019/08/31
9800
JAVA就业面试题之单例模式
双重检查模式,进行了两次的判断,第一次是为了避免不要的实例,第二次是为了进行同步,避免多线程问题。由于singleton=new Singleton()对象的创建在JVM中可能会进行重排序,在多线程访问下存在风险,使用volatile修饰signleton实例变量有效,解决该问题。
张哥编程
2024/12/21
1080
JAVA中的单例模式分析(doublecheck和枚举实现)
所为饿汉模式,即一开始就创建一个静态的对象,之后该对象一直存在。这种模式不会有线程安全问题。
冬天里的懒猫
2020/08/03
8220
Java设计模式之单例模式
在软件工程中,单例模式是一种常用的设计模式,其核心目标是确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。Java作为一门广泛使用的编程语言,实现单例模式是面试和实际开发中的常见需求。本文将深入探讨Java中的单例模式,包括其优缺点分析、实现方式等。
修己xj
2024/03/04
1500
Java设计模式之单例模式
我向面试官讲解了单例模式,他对我竖起了大拇指
单例模式相信大家都有所听闻,甚至也写过不少了,在面试中也是考得最多的其中一个设计模式,面试官常常会要求写出两种类型的单例模式并且解释其原理,废话不多说,我们开始学习如何很好地回答这一道面试题吧。
cxuan
2020/07/22
5890
我向面试官讲解了单例模式,他对我竖起了大拇指
Java设计模式:单例模式之六种实现方式详解(二)
单例模式(Singleton Pattern)是一种常用的软件设计模式,该模式的主要目标是确保一个类只有一个实例,并提供一个全局访问点来获取该实例。在单例模式中,类的构造函数通常是私有的,以防止其他类实例化它。同时,该类提供一个静态方法或属性来获取该类的唯一实例。
公众号:码到三十五
2024/03/19
2260
Java单例模式的不同写法(懒汉式、饿汉式、双检锁、静态内部类、枚举)[通俗易懂]
Java中单例(Singleton)模式是一种广泛使用的设计模式。单例模式的主要作用是保证在Java程序中,某个类只有一个实例存在。
全栈程序员站长
2022/09/15
3.2K0
java设计模式-单例模式详解
作为对象的创建模式,单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类。
李林LiLin
2020/09/24
8020
设计模式之单例模式
单例模式,是特别常见的一种设计模式,因此我们有必要对它的概念和几种常见的写法非常了解,而且这也是面试中常问的知识点。
烟雨星空
2020/06/16
5910
【云+社区年度征文】设计模式-单例模式(五种实现方法详解)
单例模式,属于创建类型的一种常用的软件设计模式。通过单例模式的方法创建的类在当前进程中只有一个实例(根据需要,也有可能一个线程中属于单例,如:仅线程上下文内使用同一个实例)。就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。
唔仄lo咚锵
2020/11/27
3990
相关推荐
灵活多变的单例模式
更多 >
领券
💥开发者 MCP广场重磅上线!
精选全网热门MCP server,让你的AI更好用 🚀
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验