单例模式详解

什么是单例模式

首先,单例模式是一种设计模式,按字面意思,指一个类只能创建一个对象,当创建出多个对象的时候,就会出现报错异常

单例模式为何出现?

1.资源共享:某些情况下,多个对象都需要共享一个资源,例如线程池,数据库连接。使用单例模式即可创造出一个公共资源,避免重复资源的重复创造和浪费

2.全局访问:一些对象需要在系统中被频繁访问,如日志,配置信息等。使用单例模式即可提供一个全局访问点,方便其他对象直接获取改实例对象

3.控制实例数量:在某些情况下,系统中运行存在一个实例,如窗口管理,任务管理器等。使用单例模式可以限制实例的数量,确保系统的稳定性和安全性


单例模式下的两种模式

1.饿汉模式
复制代码
//饿汉模式
class hungrySingleton{
    //一开始就创建好对象了 (十分迫切地想要创建对象)
    private static hungrySingleton hungrySingleton = new hungrySingleton();

    //通过这个方法来获取实例对象
    public static hungrySingleton getInstance(){
        return hungrySingleton;
    }

    public hungrySingleton(){

    }
}

是由代码可知,饿汉模式下,十分急于想创建出对象,故一开始就把对象创建好了,通过getInstance方法来获取对象实例


2.懒汉模式
复制代码
//懒汉模式  只有当调用方法的时候  实例才会被创建
class LazySingleton{
    //只要当别人调用方法时 才会创建实例对象 不急不慢
    private static LazySingleton Lazysingleton = null;

    public static LazySingleton getLazySingleton(){
        synchronized()
        if(Lazysingleton == null){
            //只有第一次获取时才能获取到实例对象
            Lazysingleton = new LazySingleton();
            return Lazysingleton;
        }
        return Lazysingleton;
    }
    public LazySingleton(){

    }
}

反观懒汉模式,并不是一开始就加载对象,而是当需要时,你就调用方法获得实例,显现出了它的不紧不慢,懒的特点


细节重点:

无论是懒汉模式,还是饿汉模式,我们都能注意到,无论是变量还是方法,都加了static关键字,这其中有什么说法呢?

我们知道,静态资源随着类的加载而加载,且类对象在其进程中,也是只有唯一的一份,这也就意味着类里面的静态资源,也只有独一份的存在,故static在中起到的作用为:

随着类的加载而加载,保证资源只有独一份

同时,我们也可以反过来想,如果这里的属性方法不加关键字,那么资源不就是随着对象的创建而被创建 ,可以通过实例对象.资源的方法被获取,那资源岂不是取之不尽用之不竭了,与我们的单例两字完全背道而驰

复制代码
 public static void main(String[] args) {
        hungrySingleton h = hungrySingleton.getInstance();
        hungrySingleton h1 = hungrySingleton.getInstance();
        System.out.println(h == h1);
    }

线程安全:
原子性

上述的两种模式,其中有一个存在线程安全问题,哪么到底是哪一个呢?

我们分析:

饿汉模式下,资源直接被创建出来,通过方法来获取实例,这区间只存在读操作(获得对象)

在懒汉模式下,刚开始的资源变量被赋值为null,当想获得此实例时,调用方法,但是方法中有一个if的条件判断 if(LazySingleton == null),而这里就涉及到了读操作 ,如果满足条件,对资源变量赋值,这时候就涉及到了写操作 ,显然,在既有读也有写的操作中,懒汉模式是线程不安全的!


如何解决:

解决线程安全,首先需要知道它产生线程安全的原因,这里的原因无非是既有读,又有写操作,故操作非原子性,于是我们即可以搬出synchronized进行加锁,使操作原子性

复制代码
class LazySingleton{
    //只要当别人调用方法时 才会创建实例对象 不急不慢
    private static LazySingleton Lazysingleton = null;

    public static LazySingleton getLazySingleton(){
        synchronized(LazySingleton.class){
            if(Lazysingleton == null){
                //只有第一次获取时才能获取到实例对象
                Lazysingleton = new LazySingleton();
                return Lazysingleton;
            }
            return Lazysingleton;
        }
    }
    public LazySingleton(){
    }
}

但此时,又会衍生出一个问题:每次执行getInstance方法获取实例对象,都需要加锁吗?

我们知道,加锁/释放锁都是有开销的,如果此资源被频繁地使用,每次使用都需要执行一次加锁操作,其开销也是巨大的

我们发现,当第一次执行方法后,此后的Lazysingleton便不是null了,于是在其之后调用方法的直接返回实例即可了,故我们只需要对第一次创建对象时加锁就行了,对象创建后就没必要再加锁了

复制代码
public static LazySingleton getLazySingleton(){
        if(Lazysingleton == null){
            synchronized (LazySingleton.class){
                if(Lazysingleton == null){
                    Lazysingleton =  new LazySingleton();
                    return Lazysingleton;
                }
            }
        }
        return Lazysingleton;
    }

内存可见性:

设想,当有大量线程同时通过方法来获取实例对象时,此时实例对象都被读为空,由于编译器优化,可能将已经实例化好的对象依然读成null,此时就会创建出多个实例对象


指令重排序:

什么是指令重排序呢?

比如一个操作的正常指令顺序为1 2 3,当由于编译器的优化(没错,又是它),使指令操作变成1 3 2,而这对于单线程是没什么问题,但对于多线程来说,就会出现问题了

这里我们把load 资源赋值 返回资源操作比喻成指令123

设想,线程1由于指令重排序使操作变成了132

线程1执行完指令13后 --->(此时变量还没有被赋值 ,直接被return了),这时线程2切进来了开始执行,对于线程2来说,既然线程1已经执行了3操作(return Lazysingleton),表明此时的资源为非空了,那么线程2也就直接返回资源了(return Lazysingleton)。但此时的资源并不是完整的,因为线程1的2操作还没有执行呢(Lazysingeton = new LazySingeton),所以此时t2拿到的是非法的对象,故出现问题


解决方法:

volatile

复制代码
 volatile private static LazySingleton Lazysingleton = null;

    public static LazySingleton getLazySingleton(){
        if(Lazysingleton == null){
            synchronized (LazySingleton.class){
                if(Lazysingleton == null){
                    Lazysingleton = new LazySingleton();
                    return Lazysingleton;
                }
            }
        }
        return Lazysingleton;
    }

故volatile具有两个功能:

1.解决内存可见性

2.解决指令重排序

相关推荐
木昜先生3 分钟前
知识点:深入理解 JVM 内存管理与垃圾回收
java·jvm·后端
115432031q6 分钟前
基于SpringBoot+Vue实现的旅游景点预约平台功能十三
java·前端·后端
战族狼魂10 分钟前
基于SpringBoot+PostgreSQL+ROS Java库机器人数据可视化管理系统
java·spring boot·postgresql
半个脑袋儿17 分钟前
Java线程控制: sleep、yield、join深度解析
java
猫猫头有亿点炸20 分钟前
C语言大写转小写2.0
c语言·开发语言
小智疯狂敲代码21 分钟前
Spring MVC-DispatcherServlet 的源码解析
java·面试
int0x0321 分钟前
Java中的内存"瘦身术":揭秘String Deduplication
java
半个脑袋儿22 分钟前
Java日期格式化中的“YYYY”陷阱:为什么跨年周会让你的年份突然+1?
java·后端
A达峰绮30 分钟前
设计一个新能源汽车控制系统开发框架,并提供一个符合ISO 26262标准的模块化设计方案。
大数据·开发语言·经验分享·新能源汽车
CHQIUU35 分钟前
Java 设计模式心法之第25篇 - 中介者 (Mediator) - 用“中央协调”降低对象间耦合度
java·设计模式·中介者模式