Java多线程中的单例模式

单例模式

饿汉单例模式

单例模式的基本含义:一个对象在JVM中始终只存在一个。要做到看起来比较简单:只需要给类创建一个实例即可。但实际可不是看起来这么简单。

单例模式设计:

  1. 创建一个静态变量,用于存放当前的对象地址。
  2. 提供该类实例的get方法。得是静态的
  3. 将构造方法私有化。
    设计如下:
java 复制代码
class SingleTon{
    private static SingleTon instance = new SingleTon();
    public static SingleTon getInstance(){
        return instance;
    }
    private SingleTon(){}
}

使用:

java 复制代码
public class HungrySingleton {
    public static void main(String[] args) {
        SingleTon s1 = SingleTon.getInstance();
        SingleTon s2 = SingleTon.getInstance();
        System.out.println(s1==s2); // true
    }
}

这里的s1和s2指的是同一个对象。

但是

这个对象创建的太早了,在类加载的时候就实例化了,故这种模式也称之为饿汉模式(一个人太饿了就饥不择食就吃的很早)。

懒汉单例模式

可以晚点创建对象,在用户调用getInstance方法时再创建对象。

设计如下:

java 复制代码
class LazySingle{
    private static LazySingle instance = null;
    public  static LazySingle getInstance(){
        if(instance == null) {
                if (instance == null) {
                    instance = new LazySingle();
                }
        }
        return instance;
    }
    private LazySingle(){};
}

如此,在调用getInstance方法时再创建对象,称之为懒汉单例模式。

但是这个设计会有线程安全的问题。

当多个线程都调用getInstance方法时,若都进入了if语句,那么创建的对象就不止一个了,极大浪费系统资源。

线程安全的懒汉单例模式

那么就可以使用synchronized对代码进行同步,保证线程安全。

如下:

java 复制代码
class LazySingle{
    private static LazySingle instance = null;
    public  static LazySingle getInstance(){
     // 通过synchronized加锁。
            synchronized (LazySingle.class) {
                if (instance == null) {
                    instance = new LazySingle();
                }
        }
        return instance;
    }
    private LazySingle(){};
}

加锁之后就可以保证线程安全了。但是又出现了一个问题:每次调用getIntance方法之后,都会加锁。(加锁是一个资料消耗极大的过程,故会降低效率)所以,需要在外层添加一个判断,当instance变量不为空的之后,才进行加锁并进行new操作。

故需要改进:

java 复制代码
class LazySingle2{
    private static LazySingle2 instance = null;
    public  static LazySingle2 getInstance(){
        // 外层if,主要是为了提高效率,判定是否需要加锁。
            if(instance == null) {
                synchronized (LazySingle2.class) {
                    // 这里的 if判断是否需要new对象。
                    if (instance == null) {
                        instance = new LazySingle2();
                    }
                }
        }
        return instance;
    }
    private LazySingle2(){};
}

现在基本实现了一个线程安全的单例模式了。但是还有优化的空间。

指令重排序:编译器为了提高效率,在保证逻辑不变的前提下,会优化指令的执行顺序。但是在多线程的代码中,这个前提很难保证。

当线程t1准备通过new创建该对象时。可能会将new的底层操作进行指令重排序。

例如:new的过程主要是三步:

  1. 申请堆空间
  2. 在堆空间上构造对象
  3. 将堆空间的地址返回给istance对象

但是编译器为了提高效率,会变成132的执行顺序,所以instance会指向一个不合法的空间(这个空间上的还没来得及构造对象),那么此时instance引用就不为空了。故当其他的线程t2执行getInstance方法时,外层if判断不成立,会直接返回instance引用。故t2线程得到了一个指向不合法空间的instance,导致误操作。

解决办法:给instance变量加上volatile关键字。volatile关键字会阻止编译器进行指令重排序,保证逻辑正确。

改进如下:

java 复制代码
class LazySingle3{
    private volatile static LazySingle3 instance = null;
    public  static LazySingle3 getInstance(){
        // 外层if,主要是为了提高效率,判定是否需要加锁。
        if(instance == null) {
            synchronized (LazySingle3.class) {
                // 这里的 if判断是否需要new对象。
                if (instance == null) {
                    instance = new LazySingle3();
                }
            }
        }
        return instance;
    }
    private LazySingle3(){};
}

至此:这就是终极版的java多线程的单例模式的实现。当然后续如果通过反射创建对象也可能会存在多线程的安全问题,但是这种情况太极端了,实际项目开发中几乎不会出现。故忽略该场景。

相关推荐
Z9fish7 分钟前
sse哈工大C语言编程练习20
c语言·开发语言·算法
CodeCaptain8 分钟前
nacos-2.3.2-OEM与nacos3.1.x的差异分析
java·经验分享·nacos·springcloud
萧鼎32 分钟前
Python 包管理的“超音速”革命:全面上手 uv 工具链
开发语言·python·uv
Anastasiozzzz1 小时前
Java Lambda 揭秘:从匿名内部类到底层原理的深度解析
java·开发语言
骇客野人1 小时前
通过脚本推送Docker镜像
java·docker·容器
刘琦沛在进步1 小时前
【C / C++】引用和函数重载的介绍
c语言·开发语言·c++
机器视觉的发动机1 小时前
AI算力中心的能耗挑战与未来破局之路
开发语言·人工智能·自动化·视觉检测·机器视觉
铁蛋AI编程实战1 小时前
通义千问 3.5 Turbo GGUF 量化版本地部署教程:4G 显存即可运行,数据永不泄露
java·人工智能·python
HyperAI超神经1 小时前
在线教程|DeepSeek-OCR 2公式/表格解析同步改善,以低视觉token成本实现近4%的性能跃迁
开发语言·人工智能·深度学习·神经网络·机器学习·ocr·创业创新
晚霞的不甘1 小时前
CANN 编译器深度解析:UB、L1 与 Global Memory 的协同调度机制
java·后端·spring·架构·音视频