【Java笔记】单例模式

目录

  • [1. 饿汉模式](#1. 饿汉模式)
  • [2. 懒汉模式](#2. 懒汉模式)
    • [2.1 线程不安全](#2.1 线程不安全)
    • [2.2 线程不安全原因分析](#2.2 线程不安全原因分析)
      • [2.2.1 原因](#2.2.1 原因)
      • [2.2.2 给 new SingletonLazy() 加锁](#2.2.2 给 new SingletonLazy() 加锁)
      • [2.2.3 在外层加锁](#2.2.3 在外层加锁)
      • [2.2.4 存在的问题](#2.2.4 存在的问题)
  • [3. 双重检查锁(Double Check Lock,DCL)(重要)](#3. 双重检查锁(Double Check Lock,DCL)(重要))
    • [3.1 DCL解释](#3.1 DCL解释)
    • [3.2 重要补充:volatile关键字的作用](#3.2 重要补充:volatile关键字的作用)

单例模式是保证一个类在整个应用程序中只有一个实例,同时提供一个统一的全局访问入口,避免因频繁创建对象造成内存浪费或状态不一致的问题。

1. 饿汉模式

代码:

java 复制代码
public class SingletonHungry {
    // 定义成员变量,使用static修饰保证全局唯一
    private static SingletonHungry instance = new SingletonHungry();

    // 构造方法私有化, 禁止外部实例化对象
    private SingletonHungry() {}

	// 加 static 将方法编程静态代码块,属于类,通过 类名.方法名 的方式调用
    public static SingletonHungry getInstance() {
        return instance;
    }
}

把这种类加载的时候就完成对象初始化的创建方式称为 "饿汉模式"

调用代码:

java 复制代码
    public static void main(String[] args) {
        SingletonHungry instance1 = SingletonHungry.getInstance();
        System.out.println(instance1);

        SingletonHungry instance2 = SingletonHungry.getInstance();
        System.out.println(instance2);

        SingletonHungry instance3 = SingletonHungry.getInstance();
        System.out.println(instance3);
    }

获取到的都是同一个对象

2. 懒汉模式

2.1 线程不安全

代码:

java 复制代码
public class SingletonLazy {
	// 不初始化
    private static SingletonLazy instance;

    private SingletonLazy() {
    }

    // 对外提供一个获取对象的方法
    public static SingletonLazy getInstance() {
        if (instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }
}

这种方法在单线程中得到的对象都是同一个,但是在多线程环境下会有线程安全问题!

创建10个线程,调用 SingletonLazy :

java 复制代码
public static void main(String[] args) {
    for (int i = 0; i < 10; i++) {
        Thread thread = new Thread(() -> {
            SingletonLazy instance = SingletonLazy.getInstance();
            System.out.println(instance);
        });
        thread.start();
    }
}

执行结果:

得到的对象不是同一个,存在线程安全问题。

2.2 线程不安全原因分析

2.2.1 原因


有多少个线程判断了 instance == null,就会 new 多少个对象!

2.2.2 给 new SingletonLazy() 加锁

java 复制代码
public static SingletonLazy getInstance() {
   // 第一次判断是否需要加锁
    if (instance == null) {
        synchronized (SingletonLazy.class) {
            instance = new SingletonLazy();
        }
    }
    return instance;
}

此时的代码依旧是线程不安全的!!!

原因:

有多少个线程进入了 if 代码块,就会有多少个对象被实例化!

2.2.3 在外层加锁

代码:

java 复制代码
public static SingletonLazy getInstance() {
        // 第一次判断是否需要加锁
        synchronized (SingletonLazy.class) {
            if (instance == null) {
                instance = new SingletonLazy();
            }
        }
        return instance;
    }

不会出现线程安全问题了。

有两个线程 t1 和 t2 ,t1 先拿到锁资源,只有线程 t1 全部执行完释放锁之后 t2 才有可能拿到锁资源,但此时 对象已经不为 null 了,就不会进入 if 代码块,就不会再次创建对象。

2.2.4 存在的问题

  1. 当第一个线程进入这个方法时,如果变量没有初始化,则获取锁进行初始化操作,此时单例对象被第一个线程创建完成;
  2. 后面的线程以后也永远不会再执行new对象的操作;
  3. synchronized还有没有必要加了?
    当第一个线程把对象创建好之后,就没有必要了,从第二个线程开始这个加锁解锁都是无效的操作,synchronized 关键字对应了CPU中的指令,LOCK 和 UNLOCK 对应的锁指令是互斥锁,比较消耗系统资源。

解决办法:在加锁前再次判断一下是否需要加锁

3. 双重检查锁(Double Check Lock,DCL)(重要)

3.1 DCL解释

代码:

java 复制代码
public class SingletonDCL {

    private static volatile SingletonDCL instance;

    private SingletonDCL() {
    }
 
    // 对外提供一个获取对象的方法
    public static SingletonDCL getInstance() {
        // 第一次判断是否需要加锁
        if (instance == null) {
            synchronized (SingletonDCL.class) {
                if (instance == null) {
                    instance = new SingletonDCL();
                }
            }
        }
        return instance;
    }
}

解析:

  1. 有 t1 和 t2 两个线程,假设t1、t2 同时进入了 if 代码块并判断 instance == null, t1 先拿到了锁资源,再次判断instance 为 null,则创建了一个对象,此时 instance 不为空了,释放锁资源后返回了这个 instance;
  2. t2 拿到锁资源,此时 instance 不为空了,则不会进入第二个 if 代码块,直接释放锁资源返回已经创建好了的对象;
  3. 当有其他线程再次获取对象时,instance 不为空,则不会进入第一层 if 代码块,直接返回已经创建好了的对象,保证了单例。

3.2 重要补充:volatile关键字的作用

java 复制代码
private static volatile SingletonDCL instance;

只要在多线程环境中修改了共享变量就要加 volatile ,主要是考虑到指令重排序的问题

new 一个对象的步骤:

  1. 在内存中申请一片空间
  2. 初始化对象的属性(赋初值)
  3. 把对象在内存中的首地址赋值给对象的引用

1 和 3 是强相关的,只有在分配完内存空间之后才会执行 3,但2并不是强相关的,可能会发生指令重排序

正常执行顺序:1、2、3

可能的重排序后顺序:1、3、2

重排序之后,在分配完内存空间后直接把对象在内存中的首地址赋值给对象的引用,此时的 instance 是一个尚未初始化完成的对象,其他线程如果访问这个未初始化完成的对象,就会导致出现错误!因此要加 volatile 禁止指令重排序!

同时也保证了可见性​,确保一个线程修改了 instance 的值后,其他线程能立即看到最新值。

相关推荐
青山木1 分钟前
Hot 100 --- LRU 缓存
java·数据结构·算法·leetcode·链表·缓存·哈希
花生了什么事o3 分钟前
Java 线程池:从参数到拒绝策略
java·jvm
大明者省8 分钟前
四大模态大模型训练体系全解析(架构+范式+分布式+算力成本·)
笔记·分布式·架构
长孙豪翔14 分钟前
引发事件的问题
java·linux·数据库
十月的皮皮14 分钟前
C语言学习学习笔记20260704-中缀表达式求值(双栈法)
c语言·笔记·学习
happyprince15 分钟前
09-vLLM KV Cache 系统完整分析
java·spring·vllm
掉鱼的猫16 分钟前
ReActAgent 使用指南:构建会思考、能行动的 AI Agent
java·llm·agent
pp起床18 分钟前
黑马点评 - 短信验证码登录实现
java·开发语言·tomcat
智者知已应修善业20 分钟前
【 LM358AD方波】2024-12-31
驱动开发·经验分享·笔记·硬件架构·硬件工程
什仙23 分钟前
电感规格书全部专业术语完整释义(村田原厂标准定义)
笔记·电感器