머리말
현재 튜토리얼 시리즈가 공개 중이며 设计模式专题
, 공간이 더 길어질 예정입니다. 마음에 드신다면 팔로우 부탁드립니다❤️ ~
이번 편에서는 디자인 패턴에 대해 알려드릴께요 单例模式
~
이 항목의 모든 사례 코드는 주로 Java
언어를 기반으로 합니다.
싱글톤 패턴
이전 섹션에서는 디자인 패턴의 기본 개념을 살펴보았고, 이 섹션에서는 디자인 패턴을 함께 구현해 보겠습니다 单例模式
.
单例模式
只有一个实例
클래스를 보장 하고 제공하는 생성 디자인 패턴입니다 全局访问点
.
单例模式
시스템에 인스턴스가 하나만 있는지 확인하고 제공해야 하는 시나리오에 적합 一个全局访问点
합니다 线程池、日志系统、配置文件管理器
.
간단한 예를 살펴보겠습니다.
게으른 스타일(안전하지 않은 스레드)
public class Singleton01 {
private static Singleton01 instance;
private Singleton01() {
// 构造函数私有化,确保只能通过getInstance()方法获取实例
}
public static Singleton01 getInstance() {
if (instance == null) {
System.out.println("instance = null");
instance = new Singleton01();
}
return instance;
}
public static void main(String[] args) {
Singleton01 singleton01 = Singleton01.getInstance();
System.out.println(singleton01.hashCode());
Singleton01 singleton02 = Singleton01.getInstance();
System.out.println(singleton02.hashCode());
System.out.println(singleton01.equals(singleton02));
}
}
复制代码
다음을 실행합니다.
instance = null
460141958
460141958
true
复制代码
결과적으로 동일한 인스턴스 개체입니다.
이 구현에서는 외부 세계가 생성자를 배치하여 인스턴스를 생성하기 위해 私有化
통과할 수 없도록 합니다 . new操作符
이 getInstance()
방법은 지연 로딩을 통해 인스턴스를 생성하는 방법을 제공하여 인스턴스가 사용될 때만 생성되도록 全局访问点
하여 리소스를 절약합니다.需要
여기서 주의할 점은 getInstance()
메서드는 메서드이므로 변수 静态
는 .instance
静态变量
위의 패턴을 라고도 懒汉式
합니다 线程不安全
~
배고픈 중국 스타일(스레드 안전)
위의 불안정한 문제가 주로 어디에 존재합니까?
스레드 보안 문제는 주로 instance
여러 번 인스턴스화되기 때문에 직접 인스턴스화 instance
방법은 스레드 보안을 유발하지 않습니다. 하지만 자원 낭비
// 饿汉式
private static Singleton01 instance = new Singleton01();
复制代码
게으른(스레드 안전)
为了确保线程安全,那有什么办法让懒汉式线程安全呢?我们只需要对getInstance()
方法进行同步加锁,那么在一个时间点只能有一个线程能够进入该方法,从而避免了被多次实例化,因为加了锁
,所以线程进入方法的时候就需要进行等待,性能
上就会有有一点损耗
public static synchronized Singleton01 getInstance() {
if (instance == null) {
System.out.println("instance = null");
instance = new Singleton01();
}
return instance;
}
复制代码
双重校验锁(线程安全)
instance
只需要被实例化一次之后就可以直接使用了。加锁
操作只需要对实例化
那部分的代码进行,只有当 instance
没有被实例化时,才需要进行加锁。双重校验锁
先判断 instance
是否已经被实例化,如果没有被实例化,那么才对实例化语句进行加锁
。下面看下代码实现:
public class Singleton02 {
private volatile static Singleton02 instance;
private Singleton02() {
}
public static Singleton02 getInstance() {
if (instance == null) {
synchronized (Singleton02.class) {
if (instance == null) {
instance = new Singleton02();
}
}
}
return instance;
}
}
复制代码
同时,我们也可以看到使用了volatile
关键字,这个在之前的文章给大家详细讲过。这里简单给大家提一下,为什么用它~
在Java
中,由于JVM
存在指令重排序
和线程可见性
的问题,当一个线程在使用一个对象的时候,另外一个线程可能会看到一个不完整
的对象状态,导致程序出现一些意想不到的错误。这个问题在多线程环境下非常常见。
为了解决这个问题,Java
提供了一种关键字叫做volatile
,它可以禁止JVM指令重排
。它可以确保变量的可见性和有序性
。在多线程环境下,当一个线程修改了volatile
变量时,它会立即刷新到主存
中,而其他线程在访问该变量时会强制从主存中重新读取最新
的值,从而避免了读取到不完整的对象状态。
在单例模式
的实现中,由于instance
变量在getInstance()
方法中被多个线程共享
,因此需要使用volatile
关键字来确保变量的可见性和有序性
,从而避免了多线程环境下的并发访问
问题。
思考一下,这里为啥要使用两个if
语句,明明在最外层已经判断了if (instance == null)
而且里边已经加了锁
了,在里边为什么还要if
判断呢?
有时候,面试官会这么问?有的同学就答不上来了。大家不妨想象一下,当两个线程同时进入加锁的方法内,在没有判断的情况下instance
对象还是会被实例化2
次,因为代码块的语句是正常执行的,只是执行先后的问题~
静态内部类(线程安全)
当 Singleton03
类加载时,静态内部类 Singleton
没有被加载进内存。只有当调用 getInstance()
方法从而触发 Singleton.INSTANCE
时 Singleton
才会被加载,此时初始化 INSTANCE
实例。这种方式不仅具有延迟
初始化的好处,而且由虚拟机提供了对线程安全
的支持。
public class Singleton03 {
private Singleton03() {
}
private static class Singleton {
private static final Singleton03 INSTANCE = new Singleton03();
}
public static Singleton03 getInstance() {
return Singleton.INSTANCE;
}
}
复制代码
枚举模式 (线程安全,最佳实践)
使用枚举实现
的单例模式
是一种简洁而又安全
的方式,这种方式可以避免多线程环境下的并发问题,同时也可以防止反射和反序列化攻击
。
在使用枚举实现单例模式时,只需要定义一个枚举类型,并在其中定义一个单例对象即可。由于枚举类型在Java
中是天然的单例模式
,因此这种方式可以保证在任何情况下都只创建一个实例对象
。
public enum Singleton04 {
INSTANCE;
private String message = "Hello World!";
public void showMessage() {
System.out.println(message);
}
}
复制代码
调用:
public class Application {
public static void main(String[] args) {
Singleton04.INSTANCE.showMessage();
}
}
复制代码
输出:
Hello World!
复制代码
反射 & 反序列化攻击
反射攻击
有的小伙伴可能不知道,这里给大家扩展一下,下面通过一个简单的例子,看了之后就会明白了
反射攻击和反序列化攻击
是两种常见的安全问题,它们都可以被用来攻击单例模式
的实现。
反射攻击
是指通过Java
的反射机制
来获取类的私有构造方法,然后通过构造方法创建类的实例对象,从而破坏单例模式的实现。由于Java
的反射机制可以访问私有
的构造方法,因此攻击者可以通过这种方式来创建
多个实例对象,从而破坏单例模式的唯一性。
public class Singleton05 {
private static Singleton05 instance = new Singleton05();
private Singleton05() {
if (instance != null) {
throw new IllegalStateException("Singleton already initialized");
}
}
public static Singleton05 getInstance() {
return instance;
}
public static void main(String[] args) throws Exception {
Constructor<Singleton05> constructor = Singleton05.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton05 instance1 = constructor.newInstance();
Singleton05 instance2 = Singleton05.getInstance();
System.out.println(instance1 == instance2);
}
}
复制代码
运行一下:
// Exception in thread "main" java.lang.reflect.InvocationTargetException
// at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
// at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
// at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
// at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
// at com.java.design.single.Singleton05.main(Singleton05.java:26)
// Caused by: java.lang.IllegalStateException: Singleton already initialized
// at com.java.design.single.Singleton05.<init>(Singleton05.java:15)
// ... 5 more
复制代码
好家伙,直接干报错,原因也很简单,因为利用反射修改了构造方法
的访问权限,然后进行了实例化,当再次运行进入if (instance != null)
就会抛出异常
序列化攻击
反序列化攻击
是指攻击者通过序列化
和反序列化
技术来破坏单例模式的实现。攻击者可以通过序列化
和反序列化
来创建多个实例
对象,从而破坏单例模式的唯一性。这种攻击方式常常被用于分布式系统中,攻击者可以在一个系统中序列化一个对象,然后在另一个系统中反序列化该对象,从而创建多个实例对象。
下面通过一个简单例子来看一下:
public class Singleton06 implements Serializable {
private static final long serialVersionUID = 1L;
private static Singleton06 instance = new Singleton06();
private Singleton06() {
if (instance != null) {
throw new IllegalStateException("Singleton already initialized");
}
}
public static Singleton06 getInstance() {
return instance;
}
public static void main(String[] args) throws Exception {
Singleton06 instance1 = Singleton06.getInstance();
// 将实例对象序列化到文件中
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("singleton.ser"));
out.writeObject(instance1);
out.close();
// 从文件中反序列化出实例对象
ObjectInputStream in = new ObjectInputStream(new FileInputStream("singleton.ser"));
Singleton06 instance2 = (Singleton06) in.readObject();
in.close();
System.out.println(instance1 == instance2); // false
}
}
复制代码
输出为 false
从而达到了破坏,既然问题知道了,那怎么去防止攻击呢?其实很简单, 为了防止反序列化攻击,可以在单例类中添加一个readResolve()
方法,用来替换从反序列化流中反序列化出的对象,确保只有单例对象的引用被返回
public class Singleton06 implements Serializable {
private static final long serialVersionUID = 1L;
private static Singleton06 instance = new Singleton06();
private Singleton06() {
if (instance != null) {
throw new IllegalStateException("Singleton already initialized");
}
}
public static Singleton06 getInstance() {
return instance;
}
// 保护措施
protected Object readResolve() {
return instance;
}
public static void main(String[] args) throws Exception {
Singleton06 instance1 = Singleton06.getInstance();
// 将实例对象序列化到文件中
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("singleton.ser"));
out.writeObject(instance1);
out.close();
// 从文件中反序
// 从文件中反序列化出实例对象
ObjectInputStream in = new ObjectInputStream(new FileInputStream("singleton.ser"));
Singleton06 instance2 = (Singleton06) in.readObject();
in.close();
System.out.println(instance1 == instance2); // true
}
}
复制代码
看下输出: true
,在这个示例代码中,我们添加了一个readResolve()
方法,该方法返回单例对象的引用
。当从反序列化流中反序列化
出一个对象时,该方法会被自动调用
,从而确保只有单例对象的引用
被返回。
结束语
下节给大家讲工厂模式
~
本着把自己知道的都告诉大家,如果本文对您有所帮助,点赞+关注
鼓励一下呗~
相关文章
项目源码(源码已更新 欢迎star⭐️)
Kafka 专题学习
- 一起来学kafka之Kafka集群搭建
- 一起来学kafka之整合SpringBoot基本使用
- 一起来学kafka之整合SpringBoot深入使用(一)
- 一起来学kafka之整合SpringBoot深入使用(二)
- 一起来学kafka之整合SpringBoot深入使用(三)
项目源码(源码已更新 欢迎star⭐️)
ElasticSearch 专题学习
项目源码(源码已更新 欢迎star⭐️)
往期并发编程内容推荐
- Java多线程专题之线程与进程概述
- Java多线程专题之线程类和接口入门
- Java多线程专题之进阶学习Thread(含源码分析)
- Java多线程专题之Callable、Future与FutureTask(含源码分析)
- 面试官: 有了解过线程组和线程优先级吗
- 面试官: 说一下线程的生命周期过程
- 面试官: 说一下线程间的通信
- 面试官: 说一下Java的共享内存模型
- 面试官: 有了解过指令重排吗,什么是happens-before
- 面试官: 有了解过volatile关键字吗 说说看
- 面试官: 有了解过Synchronized吗 说说看
- Java多线程专题之Lock锁的使用
- 面试官: 有了解过ReentrantLock的底层实现吗?说说看
- 面试官: 有了解过CAS和原子操作吗?说说看
- Java多线程专题之线程池的基本使用
- 面试官: 有了解过线程池的工作原理吗?说说看
- 面试官: 线程池是如何做到线程复用的?有了解过吗,说说看
- 面试官: 阻塞队列有了解过吗?说说看
- 面试官: 阻塞队列的底层实现有了解过吗? 说说看
- 面试官: 同步容器和并发容器有用过吗? 说说看
- 面试官: CopyOnWrite容器有了解过吗? 说说看
- 面试官: Semaphore在项目中有使用过吗?说说看(源码剖析)
- 面试官: Exchanger在项目中有使用过吗?说说看(源码剖析)
- 面试官: CountDownLatch有了解过吗?说说看(源码剖析)
- 面试官: CyclicBarrier有了解过吗?说说看(源码剖析)
- 面试官: Phaser有了解过吗?说说看
- 面试官: Fork/Join 有了解过吗?说说看(含源码分析)
- 面试官: Stream并行流有了解过吗?说说看
推荐 SpringBoot & SpringCloud (源码已更新 欢迎star⭐️)
博客(阅读体验较佳)
本文正在参加「金石计划」