Vorwort
Eine Reihe von Tutorials wird derzeit veröffentlicht 设计模式专题
, und der Platz wird länger. Wenn es Ihnen gefällt, folgen Sie ihm bitte❤️ ~
In diesem Abschnitt erzähle ich Ihnen von den Designmustern 单例模式
~
Alle Fallcodes in diesem Thema basieren hauptsächlich auf Java
Sprache, na ja, fangen wir direkt an, ohne Unsinn zu reden~
Singleton-Muster
Im vorherigen Abschnitt haben Sie sich die grundlegenden Konzepte von Entwurfsmustern angesehen, und dieser Abschnitt führt Sie dazu, die Entwurfsmuster gemeinsam zu realisieren 单例模式
.
单例模式
ist ein gestalterisches Gestaltungsmuster, das für Klasse sorgt 只有一个实例
und eine bereitstellt 全局访问点
.
单例模式
Es eignet sich für Szenarien, in denen sichergestellt werden muss, dass es nur eine Instanz im System gibt, und diese bereitgestellt werden muss 一个全局访问点
, z. 线程池、日志系统、配置文件管理器
B. usw.
Schauen wir uns ein einfaches Beispiel an:
Fauler Stil (Thread unsicher)
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));
}
}
复制代码
Führen Sie Folgendes aus:
instance = null
460141958
460141958
true
复制代码
Aus dem Ergebnis geht hervor, dass es sich um dasselbe Instanzobjekt handelt.
In dieser Implementierung stellen wir 私有化
sicher, dass die Außenwelt nicht passieren kann new操作符
, um eine Instanz zu erstellen, indem wir den Konstruktor platzieren. Die getInstance()
Methode bietet eine 全局访问点
Möglichkeit, eine Instanz durch verzögertes Laden zu erstellen, um sicherzustellen, dass 需要
die Instanz nur erstellt wird, wenn sie verwendet wird, wodurch Ressourcen gespart werden.
Hier ist zu beachten, dass Variablen als Methoden deklariert werden müssen, da Methoden Methoden getInstance()
sind .静态
instance
静态变量
Das obige Muster wird auch 懒汉式
,ye 线程不安全
~ genannt
Hungriger chinesischer Stil (Fadensicherheit)
Denken Sie also darüber nach, wo bestehen die oben genannten Unsicherheitsprobleme hauptsächlich?
Das Problem der Thread-Unsicherheit ist hauptsächlich auf instance
die mehrfache Instanziierung zurückzuführen, sodass die instance
Methode der direkten Instanziierung keine Thread-Unsicherheit verursacht. verschwendet aber Ressourcen
// 饿汉式
private static Singleton01 instance = new Singleton01();
复制代码
Lazy (threadsicher)
为了确保线程安全,那有什么办法让懒汉式线程安全呢?我们只需要对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⭐️)
博客(阅读体验较佳)
本文正在参加「金石计划」