初学设计模式----单例模式
单例模式
定义:保证一个类只能被实例化一次,提供一个访问它的全局访问点。
特点:
只有一个实例,需要获取的时候,如果该实例已经创建,则返回该实例,如果没有创建则创建一个实例保存并返回。
核心是创建一个唯一对象。在Javascript中这个及其简单,在全局创建一个对象即可;也可以自定义创建某一实例,保证在其他地方使用时保证唯一性。
属于创建型模式
优点:
内存中只有一个实例,减少了内存的开销。在频繁使用时,省去了大量时间
避免了对资源的重复占用
缺点:
违反了单一职责原则,一个类应该只关注内部逻辑,而不应该关心外部实现
简答的单例模式设计开发比较简单,但是复杂的单例模式需要考虑线程安全等并发问题,引入了部分复杂度
由于提供了一种单点访问,所以导致模块之间的耦合性增强,不利于单元测试
使用场景:
例如vuex中的store等需要唯一的实例
需要在全局使用的唯一的对象实例,如购物车、登录框、第三库等
需要缓存的信息可以用单例模式来实现
设计实现:
考虑的因素:线程安全、延迟加载、代码安全(防止序列化攻击、防止反射攻击等)、性能因素
实现方式:
饿汉式:在单例对象实例进行声明引用的时候就进行实例化创建对象实例,常用方式
懒汉式(线程安全、线程非安全):单例类对象实例懒加载。不会提前创建对象,只有在使用对象实例的时候才会创建
双重检查(DCL:Double Checked Locking):又称双检锁,即对实例对象进行两次检查
静态内部类
枚举:如果有关于反序列化创建对象会考虑使用枚举实现单例模式
实现方式 | 线程安全 | 并发性能好 | 可延迟加载 | 序列化/反序列化安全 | 防反射攻击 |
---|---|---|---|---|---|
饿汉式 | Y | Y | N | N | N |
懒汉式(不加锁) | N | Y | Y | N | N |
懒汉式(加锁) | Y | N | Y | N | N |
DCL | Y | Y | Y | N | N |
静态内部类 | Y | Y | Y | N | N |
枚举 | Y | Y | N | Y | Y |
例子:
// 1.饿汉模式
class Singleton {
static instance = new Singleton()
construtor(){}
getInstance(){
return this.instance
}
}
// 饿汉模式
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
以上都是通过getInstance获取对象,保证了单例对象的唯一。
// 2.懒汉模式
class Singleton {
constructor() {
if (!Singleton.instance) {
Singleton.instance = this;
}
return Singleton.instance;
}
}
const single = new Singleton()
const single2 = new Singleton()
console.log(single === single2); // true
// 懒汉式(线程不安全,不可用)
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
// 懒汉模式中最简单的一种写法,只有在第一次访问时才会实例化,达到了懒加载的效果。但是在多线程时,会出现知名问题。如果多个线程同时访问,就会出现多次实例化的结果,所以改写法不可用
// 懒汉式(线程安全,可用)
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
// 对getInstance加了锁的处理,保证在同一时刻只能有一个线程访问并获得实例
// 但缺点即使使用了synchronized修饰整个方法,每个线程访问都要进行同步,导致效率低下,下面的DCL改进了这种写法
// DCL(双重检查)
// 使用同步块加锁的方法
// 会有两次检查instance == null
// -- 一次在同步块外
// -- 一次在同步块内
// --因为会有多个线程一起进入同步块外的if中
// --如果不在同步块内不进行二次检验就会导致生成多个实例
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
说明:
volatile:
对于计算机中的指令而言 ,CPU和编译器为了提升程序的执行效率,通常会按照一定的规则对指令进行优化
如果两条指令互不依赖,那么指令执行的顺序可能不是源码的编写顺序
形如instance = new Instance(),方法创建实例执行分为三步:
分配对象内存空间: 给新创建的Instance对象分配内存
初始化对象: 调用单例类的构造函数来初始化成员变量
分配对象内存空间
初始化对象
CPU和编译器在指令重排时,不会关心指令重排执行是否影响多线程的执行结果. 如果不加volatile关键字,如果有多个线程访问getInstance()方法时,如果刚好发生了指令重排,可能会出现以下情况:
如果此时有另一个线程调用getInstance() 方法,在第一个if的判断时结果就为false, 就会直接返回没有初始化完成的instance, 这样可能会导致程序NPE异常
使用volatile的原因是禁止指令重新排序:
在volatile变量进行赋值操作后会有一个内存隔离
读操作不会重排序到内存隔离之中
比如在上面操作中,读操作必须在执行完1,2,3或者1,3,2步骤之后才会执行读取到结果,否则不会读取到相关结果
// 静态内部类
public class Singleton {
private Singleton() {}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
使用静态内部类模式创建单例类实例是使用JVM机制保证线程安全:
静态单例对象没有作为单例类的成员变量直接实例化,所以当类加载时不会实例化单例类
第一次调用getInstance() 方法时将加载静态内部类SingletonInstance. 在静态内部类中定义了一个static类型的变量instance, 这时会首先初始化这个变量
通过JVM来保证线程安全,确保该成员变量只初始化一次
由于getInstance() 方法并没有加线程锁,所以对性能没有什么影响
静态内部类的优点:
静态内部类SingletonInstance是私有的,只能通过getInstance() 方法进行访问,所以这是懒加载的
读取实例时不会进行同步锁的获取,性能较好
静态内部类不依赖JDK版本
// 枚举
public enum Singleton {
INSTANCE;
}
使用枚举方式实现单例的最大特点是非常简单。可以通过Enum.INSTANCE来访问实例,和getInstance() 方法比较更加简单
枚举的创建默认就是线程安全的方法,而且能防止反射以及反序列化导致重新创建新的对象
Enum类内部使用Enum类型判定防止通过反射创建新的对象
Enum类通过对象的类型和枚举名称将对象进行序列化,然后通过valueOf() 方法匹配枚举名称找到内存中的唯一对象实例,这样可以防止反序列化时创建新的对象
特别说明:
懒汉式和饿汉式实现的单例模式破坏 : 无论是通过懒汉式还是饿汉式实现的单例模式,都可能通过反射和反序列化破坏掉单例的特性,可以创建多个对象
反射破坏单例模式: 利用反射,可以强制访问单例类的私有构造器,创建新的对象
public static void main(String[] args) {
// 利用反射获取单例类的构造器
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
// 设置访问私有构造器
constructor.setAccessiable(true);
// 利用反射创建新的对象
Singleton newInstance = constructor.newInstance();
// 通过单例模式创建单例对象
Singleton singletonInstance = Singleton.getInstance();
// 此时这两个对象是两个不同的对象,返回false
System.out.println(singletonInstance == newInstance);
}
反序列化破坏单例模式: 通过readObject() 方法读取对象时会返回一个新的对象实例
public static void main(String[] args) {// 创建一个输出流对象
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("Singleton.file"));
// 将单例类对象写入到文件中
Singleton singletonInstance = Singleton.getInstance();
os.writeObject(singleton);
// 从文件中读取单例对象
File file = new File("Singleton.file");
ObjectInputStream is = new ObjectInputStream(new FileInputStream(file));
Singleton newInstance = (Singleton)is.readObject();
// 此时这两个对象是两个不同的对象,返回false
System.out.println(singletonInstance == newInstance);
}
参考资料:
也来谈谈懒汉和饿汉,详细解析单例模式的六种实现方式!- 掘金 (juejin.cn)
面试官所认为的单例模式 - 掘金 (juejin.cn)
JavaScript设计模式es6(23种) - 掘金 (juejin.cn)
在JS中总结一下设计模式(单例-懒汉,单例-饿汉,工厂,代理,观察者)_黯蕶-veteran的博客-CSDN博客