Java多线程案例(1)--单例模式

1.设计模式

设计模式是软件开发的前辈总结的经验,是解决重复性问题的代码结构

合理使用设计模式可以:

  • 提高代码复用性(避免重复造轮子)
  • 增强可维护性(代码结构更清晰)
  • 提升扩展性(更容易适应需求变化)

有一类设计模式是创造性模式

【问题】:如何合理地创造对象

【解决】:提供除new以外获取对象的方法

常用模式

  • 单例模式(Singleton):确保一个类只有一个实例(如数据库连接池)。
  • 工厂模式(Factory):将对象的创建逻辑封装起来(如 Calendar.getInstance())。
  • 建造者模式(Builder):分步构建复杂对象(如 StringBuilder)。
  • 原型模式(Prototype):通过克隆方式创建对象(如 Object.clone())。

2.单例模式

一个类在一个项目/板块内只允许有的一个实例对象,并提供全局访问点。

实现过程:

  1. 将类对象私有化并设置为静态对象==》确保只有一个实例对象
  2. 外部通过接口获取/访问该类对象==》外界获取类对象方法
  3. 构造方法私有化==》防止外界通过构造方法获取多个实例对象

2.1饿汉式(Eager Initialization)

public class SingletonEager {
    //将实例设为static类对象
    private static SingletonEager singletonEager=new SingletonEager();

    //给外部接口以获得单例对象
    public static SingletonEager getSingletonEager(){
        return singletonEager;
    }

    //将构造方法私有化,防止外界new出对象
    private SingletonEager(){

    }

}

Eager意为迫切的意思,在*类创建的时候就将实例对象创建出来

优点:线程安全(JVM 保证类加载时唯一性),这个我们待会聊到。

缺点:如果该实例对象没被使用,会造成资源浪费

Java文件从编写到运行

==(编写)==》.java文件

==(javac命令编译)==》.class字节码文件

*==(JVM通过类加载器)==》加载.class文件,生成class对象,初始化

==(执行阶段)==》JVM 解释执行字节码,执行 main 方法,创建对象、调用方法等

2.2懒汉式(Lazy Initialization)

public class SingletonLazy {
    //懒汉式,先将实例置为空,等到需要时再创建
    private static SingletonLazy singletonLazy=null;

    //调用时再判断是否创建
    public static SingletonLazy getSingletonLazy(){
        if (singletonLazy==null){
            singletonLazy=new SingletonLazy();
        }
        return singletonLazy;
    }

    //将构造方法私有化
    private SingletonLazy(){

    }
}

懒的意思是,当需要这个类对象的时候,再通过这个类的API获取类对象

优点:需要该实例对象再获取,不会造成资源浪费

缺点:多线程下不安全

3.单例模式下的线程安全

饿汉式:类对象在类加载时已创建,接口只有获取对象,多线程下安全

懒汉式:接口有对象创建,多线程下不安全

操作系统对线程调度是随机的,

在线程一判断类对象为空,准备创建,==(线程调度)==》……

在线程二判断类对象为空,创建类对象,==(线程调度)==》……

线程一创建类对象。

创建了两个类对象,违反了单例模式。

在main方法中获取类对象实例:

public class Test{
    public static void main(String[] args) {

        //多线程下获取实例,每个实例不一样
        for (int i = 1; i < 10; i++) {
            new Thread(()-> {
                System.out.println(SingletonLazy.getSingletonLazy());
            }).start();
        }

    }

}
    

运行代码观察结果:

Singleton.SingletonLazy@719a3f57
Singleton.SingletonLazy@44385a59
Singleton.SingletonLazy@44385a59
Singleton.SingletonLazy@44385a59
Singleton.SingletonLazy@44385a59
Singleton.SingletonLazy@44385a59
Singleton.SingletonLazy@44385a59
Singleton.SingletonLazy@44385a59
Singleton.SingletonLazy@44385a59

可以观察到有不同的类对象,违反了单例模式(对象创建前存在线程安全问题)

4.线程安全解决方法

使用锁

 private static Object locker=new Object();

 public static SingletonLazeSafety getSingletonLazeSafety(){
       
           synchronized (locker){

               if (singletonLazeSafety==null){
                   singletonLazeSafety=new SingletonLazeSafety();
               }

           }
       
       return singletonLazeSafety;
   }

画图分析,当判断可以实例化对象时,其他线程尝试创建线程会产生锁竞争导致阻塞等待

运行代码并观察结果

//线程安全懒汉式
    public static void main(String[] args) {
        for (int i = 1; i < 10; i++) {
            new Thread(()-> {
                System.out.println(SingletonLazeSafety.getSingletonLazeSafety());
            }).start();
        }
    }
Singleton.SingletonLazeSafety@2fdb3c07
Singleton.SingletonLazeSafety@2fdb3c07
Singleton.SingletonLazeSafety@2fdb3c07
Singleton.SingletonLazeSafety@2fdb3c07
Singleton.SingletonLazeSafety@2fdb3c07
Singleton.SingletonLazeSafety@2fdb3c07
Singleton.SingletonLazeSafety@2fdb3c07
Singleton.SingletonLazeSafety@2fdb3c07
Singleton.SingletonLazeSafety@2fdb3c07

5.单例模式懒汉式弊端

但是真的加上锁就完美无缺了吗??

我们知道,调用单例模式提供的API获取单例对象时不仅有初始化的时候,还有访问该类对象。

所以当类对象已经创建的时候再调用get方法获取类对象,if判断语句不满足,直接返回对象,多线程下不会有线程安全问题

但是我们的代码每次调用get方法都要获取锁资源,锁资源获取释放会设计线程阻塞等待,对于计算机来说会造成极大成本浪费

操作系统负责线程的创建、调度、状态切换(如运行、阻塞、就绪),这些操作需要内核态与用户态的切换,开销极大。
Java 的 synchronized 锁机制最终依赖操作系统的 mutex 锁(互斥锁) 实现,因此
锁的获取和释放必然涉及操作系统的线程调度

高并发场景下(如 1000个线程同时调用),会产生大量无意义的锁竞争,导致线程频繁阻塞/唤醒、上下文切换,CPU 时间大量消耗在“线程调度”而非“业务逻辑执行”上性能急剧下降

我们对无脑加锁操作进行优化:

private static Object locker=new Object();

   public static SingletonLazeSafety getSingletonLazeSafety(){
        //判断是否需要因为需要创建对象而入锁
       if (singletonLazeSafety==null){

           synchronized (locker){
               if (singletonLazeSafety==null){
                   singletonLazeSafety=new SingletonLazeSafety();
               }
           }
       }
       return singletonLazeSafety;
   }

我们会感到疑惑,为什么同一个方法中连续出现两处相同的if判断语句。

本质是确保 线程安全 的同时 对锁资源合理利用

再次画图观察多线程下调用get方法,两次if判断可能结果不同

6.指令重排序

6.1什么是指令重排序?

指令重排序是指在 程序逻辑不变的情况下,

对程序中的指令执行顺序生成机器码的顺序进行调整,以提升性能

6.2指令重排序发生在哪些层面?

6.2.1 编译器优化重排序

编译器在将高级语言(如 Java、C++)代码编译为机器码时,可能会调整指令的顺序,只要不改变程序在单线程下的行为。例如:

  • 将不相关的计算提前或延后执行

  • 重新组织加载(load)和存储(store)指令的顺序

6.2.2 CPU 执行层面的重排序

即使编译器没有重排序,现代 CPU(尤其是多核、超标量架构的 CPU)在执行指令时,也可能会乱序执行,以提高指令吞吐率。

CPU 会在保证单线程语义正确的前提下,动态地调整指令的实际执行顺序,比如:

  • 提前执行那些不依赖当前数据的指令

  • 延迟执行那些需要等待数据加载的指令

6.3为什么能提高效率?

  1. 指令之间存在依赖关系,但也有很多是独立的

    通过重排序,可以让独立的指令并行执行,提高 CPU 利用率(指令级并行,是复杂的)。

  2. 减少流水线停顿

    比如等待数据从内存加载时,CPU 可以先执行其他不依赖该数据的指令。

  3. 更好地利用缓存与预取机制

    某些数据访问顺序调整后,能更好地命中缓存,减少访问主存的延迟。

6.4get方法中指令重排序

观察上面代码的简化指令:

1.分配空间 2.初始化 3.赋值到引用

在CPU/编译器层面,为了提高并行度,它们将3(赋值引用)调到2(初始化对象)前,

他们认为:

提前将引用(instance)先写入内存,

后续代码其他指令(或线程)可以拿着引用访问对象,

这样那些依赖instance的后续代码可以提前执行,从而提高并行度和代码吞吐量

在单线程中上面的逻辑确实会提高并行度,但是在多线程中是极其危险

操作系统对线程的调度是随机的

当线程1给引用赋值(2)但没有初始化

线程2直接拿到没有初始化的引用,后续无法访问里面的变量和调用方法

假如instance内,有变量a=100,b=200,方法fun{a+b},当线程2调用fun得到0而不是300,

因为intance没有初始化

6.5解决方法

使用关键字volatile修饰变量instance

volatile的作用

1.保证指令顺序执行

JVM 会对volatile写操作建立内存屏障,禁止CPU和编译器指令重排序

==》在这里确保先初始化对象,再赋值引用

2.内存可见性

一个线程对 volatile变量的修改,会立即对其他线程可见,不会停留在 CPU 缓存中。

给类对象加上volatile修饰

   //加上volatile修饰类对象
   private static volatile SingletonLazeSafety singletonLazeSafety=null;


   private static Object locker=new Object();
   public static SingletonLazeSafety getSingletonLazeSafety(){
       if (singletonLazeSafety==null){
           synchronized (locker){
               if (singletonLazeSafety==null){
                   singletonLazeSafety=new SingletonLazeSafety();
               }
           }
       }
       return singletonLazeSafety;
   }

7.总结

        单例模式懒汉式在需要这个实例化对象时才创建,可以避免资源浪费,但是创建这个操作会在多线程中引起线程安全问题。我们通过对判断对象是否存在和创建对象进行加锁,防止在一个项目/板块中创建多个实例。但是,该线程安全只存在于对象初始化之前,当后续想访问单例对象时需要频繁入锁出锁,获取锁资源,使CPU浪费极多时间在“线程调度”而非“逻辑处理”,我们在锁获取之前加上对象存在判断,有利于合理利用锁资源。最后,由于CPU会对实例化对象的指令重排序,导致多线程下有线程获取没有初始化的对象,调用对象的成员变量和方法出错,加上volatile防止CPU对变量的指令重排序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值