单例模式详解

什么是单例模式

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

单例模式为何出现?

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.解决指令重排序

相关推荐
CS Beginner3 分钟前
【搭建】个人博客网站的搭建
java·前端·学习·servlet·log4j·mybatis
JavaTree201739 分钟前
【Spring Boot】Spring Boot解决循环依赖
java·spring boot·后端
lang201509281 小时前
Maven 五分钟入门
java·maven
cj6341181501 小时前
SpringBoot配置Redis
java·后端
用坏多个鼠标1 小时前
Nacos和Nginx集群,项目启动失败问题
java·开发语言
满天星83035771 小时前
【C++】右值引用和移动语义
开发语言·c++·redis·visual studio
消失的旧时光-19431 小时前
c语言 内存管理(malloc, calloc, free)
c语言·开发语言
歪歪1001 小时前
在C#中除了按属性排序,集合可视化器还有哪些辅助筛选的方法?
开发语言·前端·ide·c#·visual studio
TangKenny2 小时前
基于EasyExcel的动态列映射读取方案
java·easyexcel
安冬的码畜日常2 小时前
【JUnit实战3_19】第十章:用 Maven 3 运行 JUnit 测试(下)
java·测试工具·junit·单元测试·maven·junit5