1.设计模式
设计模式是软件开发的前辈总结的经验,是解决重复性问题的代码结构
合理使用设计模式可以:
- 提高代码复用性(避免重复造轮子)
- 增强可维护性(代码结构更清晰)
- 提升扩展性(更容易适应需求变化)
有一类设计模式是创造性模式
【问题】:如何合理地创造对象
【解决】:提供除new以外获取对象的方法
常用模式:
- 单例模式(Singleton):确保一个类只有一个实例(如数据库连接池)。
- 工厂模式(Factory):将对象的创建逻辑封装起来(如
Calendar.getInstance())。 - 建造者模式(Builder):分步构建复杂对象(如
StringBuilder)。 - 原型模式(Prototype):通过克隆方式创建对象(如
Object.clone())。
2.单例模式
一个类在一个项目/板块内只允许有的一个实例对象,并提供全局访问点。
实现过程:
- 将类对象私有化并设置为静态对象==》确保只有一个实例对象
- 外部通过接口获取/访问该类对象==》外界获取类对象方法
- 构造方法私有化==》防止外界通过构造方法获取多个实例对象
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为什么能提高效率?
-
指令之间存在依赖关系,但也有很多是独立的
通过重排序,可以让独立的指令并行执行,提高 CPU 利用率(指令级并行,是复杂的)。
-
减少流水线停顿
比如等待数据从内存加载时,CPU 可以先执行其他不依赖该数据的指令。
-
更好地利用缓存与预取机制
某些数据访问顺序调整后,能更好地命中缓存,减少访问主存的延迟。
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对变量的指令重排序。

--单例模式&spm=1001.2101.3001.5002&articleId=154425508&d=1&t=3&u=853bd5ee6fa540cebf76237bc96d050a)
2950

被折叠的 条评论
为什么被折叠?



