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

相关推荐
云烟成雨TD18 小时前
Spring AI Alibaba 1.x 系列【6】ReactAgent 同步执行 & 流式执行
java·人工智能·spring
Wenweno0o18 小时前
0基础Go语言Eino框架智能体实战-chatModel
开发语言·后端·golang
于慨18 小时前
Lambda 表达式、方法引用(Method Reference)语法
java·前端·servlet
swg32132118 小时前
Spring Boot 3.X Oauth2 认证服务与资源服务
java·spring boot·后端
gelald18 小时前
SpringBoot - 自动配置原理
java·spring boot·后端
殷紫川18 小时前
深入理解 AQS:从架构到实现,解锁 Java 并发编程的核心密钥
java
一轮弯弯的明月18 小时前
贝尔数求集合划分方案总数
java·笔记·蓝桥杯·学习心得
chenjingming66618 小时前
jmeter线程组设置以及串行和并行设置
java·开发语言·jmeter
殷紫川18 小时前
深入拆解 Java volatile:从内存屏障到无锁编程的实战指南
java
eddieHoo18 小时前
查看 Tomcat 的堆内存参数
java·tomcat