单例模式:饿汉模式、懒汉模式

简介: 单例模式:饿汉模式、懒汉模式



一、什么是单例模式

单例模式是Java中的设计模式之一,能够保证某个类在程序中只存在唯一一份实例,而不会创建出多个实例

单例模式有很多实现方式,最常见的是饿汉和懒汉两种模式

二、饿汉模式

饿汉模式在类加载时就创建实例

如何实现饿汉式单例模式?

1. 在类中完成实例的初始化,在创建的类中创建唯一实例

2. 对外提供获取该唯一实例的方法,提供访问该实例的全局静态方法getInstance(),来获取该类的唯一实例

3. 构造方法私有化,保证类外部不能实例化,只有在类中创建的唯一实例

//饿汉式单例模式
public class HungrySingLeton {
    //在类中创建唯一实例
    private static final HungrySingLeton instance = new HungrySingLeton();
    
    //构造方法私有化,保证类外部不能进行实例化
    private HungrySingLeton(){}
    //对外提供获取该唯一实例的方法
    public static HungrySingLeton getInstance(){
        return instance;
    }
}

饿汉式单例模式线程安全吗?

饿汉式单例模式在类加载时就进行初始化,创建唯一实例。它在线程还没出现之前就实例化了,外部只能通过getInstance()方法来获取唯一实例,相当于“读操作”,因此是线程安全的

饿汉式单例模式的缺点

在类加载时就创建实例,并一直在内存中,若不使用该实例,该实例仍然存在,此时存在内存浪费问题

三、懒汉模式

类加载时不创建实例,直到第一次使用的时候才创建实例

如何实现懒汉模式?

懒汉模式的实现与饿汉模式类似,唯一的区别是懒汉模式直到第一次使用的时候才会创建实例

1. 在类中创建唯一实例,并将该实例的初始值设为null

2. 对外提供获取该唯一实例的方法,若是第一次使用该方法,则创建实例

3.  构造方法私有化,保证类外部不能实例化,只有在类中创建的唯一实例

public class LazySingleton {
    //在类中创建唯一实例,并将其置为null
    private static LazySingleton instance = null;
    
    //构造方法私有化,保证类外部不能进行实例化
    private LazySingleton(){}
    
    //对外提供获取该唯一实例的方法
    public static LazySingleton getInstance(){
        //若是第一次使用该方法,则初始化instance
        if (instance == null){
            instance = new LazySingleton();
        }
        return instance;
    }
    
}

懒汉模式线程安全吗?

在多线程情况下,可能会出现创建多个实例的情况

如何解决线程安全问题?

通过加锁,来解决线程安全问题

将判断instance是否为空,和创建实例两个操作加上锁,或是直接在方法上加上synchronized,从而保证在上图的情况下,也只创建一个实例

public static LazySingleton getInstance(){
        synchronized (LazySingleton.class){
            //若是第一次使用该方法,则初始化instance
            if (instance == null){
                instance = new LazySingleton();
            }
        }
        return instance;
    }

或

public synchronized static LazySingleton getInstance(){
        //若是第一次使用该方法,则初始化instance
        if (instance == null){
            instance = new LazySingleton();
        }
        return instance;
    }

此时

由于加锁和解锁开销较高,而懒汉式单例模式仅在第一次调用时,才会存在可能创建多个实例的问题,在后面调用getInstance()方法时,判断instance不为空,直接返回instance,而在加锁后,无论是否已经存在实例,在多线程情况下都会发生阻塞,此时存在执行效率低的问题

因此,在加锁前,判断是否已经创建实例,若已经创建实例,则直接返回instance,若未创建实例,则进行加锁操作

public static LazySingleton getInstance(){
        //判断是否已经创建实例,若已经创建实例,则不加锁,直接返回instance
        if(instance == null){
            //若实例未创建,则向下执行来竞争锁
            //竞争成功的锁,进行创建实例操作
            synchronized (LazySingleton.class){
                //在竞争成功的锁创建实例并释放锁后
                //其他竞争到锁的线程被内层if挡住,不会创建多个实例
                if (instance == null){
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }

通过两个if条件判断,降低了锁竞争的频率,既保证了线程安全,又提高了执行效率

此时线程安全了吗?

此时,指令重排序,也可能引起线程安全问题

指令重排序,是编译器优化的一种方式,通过调整原有代码的执行顺序,在保证逻辑不变的前提下,提高程序的效率

在创建实例时,可将其分为三个步骤

1. 申请一段内存空间

2. 在该内存空间上调用构造方法,创建出实例

3. 将该内存地址赋值给instance引用变量

正常情况下,创建实例是按照1 2 3的顺序来执行的,而编译器也可能会将其优化为1 3 2的顺序来执行

而当按照1 3 2 的方式来创建实例时,就可能会出现问题

如何解决指令重排序带来的线程安全问题?

使用volatile

volatile能够禁止指令重排序,从而保证创建实例时,按照1 2 3的顺序来创建出实例,保证创建出初始化的实例

public class LazySingleton {
    //在类中创建唯一实例,并将其置为null
    //volatile:禁止指令重排序
    private volatile static LazySingleton instance = null;
    //构造方法私有化,保证类外部不能进行实例化
    private LazySingleton() {
    }
    //对外提供获取该唯一实例的方法
    public static LazySingleton getInstance() {
        //判断是否已经创建实例,若已经创建实例,则不加锁,直接返回instance
        if (instance == null) {
            //若实例未创建,则向下执行来竞争锁
            //竞争成功的锁,进行创建实例操作
            synchronized (LazySingleton.class) {
                //在竞争成功的锁创建实例并释放锁后
                //其他竞争到锁的线程被内层if挡住,不会创建多个实例
                if (instance == null) {
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }
}

懒汉模式在类加载时,并没有进行实例化,而是在第一次调用getInstance()方法的时候,才进行实例化。若一直没有调用getInstance()方法,则不创建该唯一实例,此时节省了实例化的开销

目录
相关文章
|
SQL XML Java
一文搞懂Mybatis执行原理
一文搞懂Mybatis执行原理
748 1
|
12月前
|
机器学习/深度学习 人工智能 前端开发
终端里的 AI 编程助手:OpenCode 使用指南
OpenCode 是开源的终端 AI 编码助手,支持 Claude、GPT-4 等模型,可在命令行完成代码编写、Bug 修复、项目重构。提供原生终端界面和上下文感知能力,适合全栈开发者和终端用户使用。
61644 11
|
SQL 人工智能 移动开发
Android Logcat 命令行工具
Logcat 是一个命令行工具,用于转储系统消息日志,包括从您的应用使用 Log 类写入的消息。 logcat还是比较简单的,我们在此只记录一下我们日常工作可能会用的比较多的一些用法。
Android Logcat 命令行工具
|
算法 安全 Java
Java并发基石ReentrantLock:深入解读其原理与实现
Java并发基石ReentrantLock:深入解读其原理与实现
|
安全 API Android开发
Android全局广播+本地广播
本文详细介绍了Android中的全局广播与本地广播的使用方法及其注意事项。针对Android 8.0及以上版本广播机制的变化,文章分析了静态注册失效、跨应用广播无法接收及广播接收顺序问题,并提供了相应解决方案,如通过`setPackage()`指定包名和避免静态与动态注册共存。此外,文章还深入讲解了LocalBroadcastManager的使用场景与优势,强调其在应用内通信中的高效性和安全性,同时对比了全局广播与本地广播的区别,为开发者提供了清晰的实践指导。
622 0
|
消息中间件 缓存 中间件
缓存一致性问题,这么回答肯定没毛病!
缓存一致性问题,这么回答肯定没毛病!
576 3
|
Java Windows
bat启动jar包时,如何设置jdk
【9月更文挑战第3天】bat启动jar包时,如何设置jdk
1840 5
|
算法 安全 Java
【经典算法】LeetCode 21:合并两个有序链表Java/C/Python3实现含注释说明,Easy)
【经典算法】LeetCode 21:合并两个有序链表Java/C/Python3实现含注释说明,Easy)
683 1

热门文章

最新文章