【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 的值后,其他线程能立即看到最新值。

相关推荐
毕设源码-郭学长6 小时前
【开题答辩全过程】以 基于SpringBoot技术的美妆销售系统为例,包含答辩的问题和答案
java·spring boot·后端
梨落秋霜7 小时前
Python入门篇【文件处理】
android·java·python
Java 码农7 小时前
RabbitMQ集群部署方案及配置指南03
java·python·rabbitmq
哈库纳玛塔塔7 小时前
放弃 MyBatis,拥抱新一代 Java 数据访问库
java·开发语言·数据库·mybatis·orm·dbvisitor
S***q3778 小时前
Spring Boot管理用户数据
java·spring boot·后端
天“码”行空8 小时前
java面向对象的三大特性之一多态
java·开发语言·jvm
毕设源码-郭学长8 小时前
【开题答辩全过程】以 基于SpringBoot框架的民俗文化交流与交易平台的设计与实现为例,包含答辩的问题和答案
java·spring boot·后端
好大哥呀9 小时前
Java Web的学习路径
java·前端·学习
f***14779 小时前
SpringBoot实战:高效实现API限流策略
java·spring boot·后端
on the way 1239 小时前
day06-SpringDI 依赖注入
java·spring