设计模式系列(二):单例模式,看这篇就够了~

文章目录

    • 什么是单例模式?
    • 单例模式的优缺点和使用场景
    • 饿汉式
    • 懒汉式
    • 双端检索机制
    • 双端检索机制 + volatile 关键字
    • 单例模式的其他实现方式
      • 静态内部类方式
      • 枚举方式
    • 单例模式实现方式总结及对比

什么是单例模式?

单例模式所有设计模式中的 创建型模式
保证一个类从始到终仅有一个实例,同时这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要手动实例化该类的对象。

单例模式的核心:

1. 该类只能有一个实例
2. 该类必须自己创建自己的唯一实例。
3. 该类必须给所有其他对象提供这一实例。
4. 该类的构造函数必须是私有的。

单例模式的优缺点和使用场景

优点

  1. 减少内存开销,提高系统性能。避免对象频繁的创建与销毁。
  2. 避免对资源的多重占用。例如写文件操作,由于只有一个实例存在内存中,避免对同一资源文件的同时写操作。

缺点

  1. 与单一职责原则冲突。
  2. 单例类没有接口,而且不能被继承。所以扩展性差。

使用场景

  1. 需要频繁实例化然后销毁的对象。
  2. 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。

常见使用场景:计数器、数据库连接池、线程池、Spring相关Bean 比如 Service、Controller 默认就是单例的。

饿汉式

顾名思义,在初始化时 就进行对象的实例化

public class Instance {
	// 私有静态 的 对象实例
	private static Instance  instance = new Insance();
	// 私有化构造方法
	private Instance () {
	}
	// 获取 实例的 方法
	public static Instance getInstance() {
		return instance;
	}
}

懒汉式

在第一次用到的时候才进行实例化

public class Instance {
	private static Instance  instance;
	// 私有化构造方法
	private Instance () {
	}
	// 获取 实例的 方法
	public static Instance getInstance() {
		if(instance == null) {
			instance = new Instance();
		}
		return instance;
	}
}

懒汉式在多线程下的线程安全问题

假设有两个线程A ,B,同时调用getInstance()方法。

1. 线程A执行到 if(instance == null) {   这行代码,条件成立
2. 线程A进入执行instance = new Instance();
3. 这时 线程B 执行到 if(instance ==null) {   这行代码,
4. 此时线程A 执行instance = new Instance();还没有结束
5. 所以线程B 判断 if(instance ==null)条件也成立
6. 那么线程B也 进入执行instance = new Instance();
7. 这就会 导致两个线程返回的不是同一个对象。造成了线程安全问题。

解决方式:

  1. 对方法加锁 public static synchronized Instance getInstance() {}
  2. 对代码块加锁 synchronized(Instance.class){ … }

但是仔细思考一下,如果直接加锁的话,每次都只能一个线程执行的话,岂不是会导致效率极低?有没有 既可以保证线程安全 又在一定程度上提高效率 的方式,看下面:

双端检索机制

public class Instance {
	private static Instance  instance;
	// 私有化构造方法
	private Instance () {
	}
	// 获取 实例的 方法
	public static Instance getInstance() {
		if(instance == null) {
			// 先判断一次,在进行加锁,比直接加锁,效率要更高。
			synchronized(Instance.class){
				if(instance == null) {
					instance = new Instance();
				}
			}	
		}
		return instance;
	}
}

到这里,懒汉式的单例模式就比较完美了。但是还是有一个小的缺陷。这里就要说一下指令重排的问题了。

双端检索机制存在的指令重排问题

首先来了解一下对象的实例化过程:

Instance instance = new Instance(); 这句代码一共包含几步操作?

1. memory = allocate(); 分配对象内存空间
2. instance (memory); 初始化对象
3. instance = memory; 设置 instance 指向刚分配的内存地址,此时 instance != null

这是正常情况下对象的实例化过程。分配空间 =》初始化对象 =》引用指向内存地址

指令重排

但是,JVM可能在底层做自动优化。叫做指令重排。在指令重排的情况下,上述步骤 就可能会变为 1 =》3 =》2。也就是 分配空间 =》引用指向内存地址 =》初始化对象。 那么在执行步骤2(引用指向内存地址)时,instance != null,但是实际上对象还没有初始化完成。

场景:假设两个线程A,B

1. 线程A执行到 if(instance == null) {   这行代码,条件成立
2. 线程A进入执行instance = new Instance(); 这时JVM做了指令重排序。
3. 这时 线程B 执行到 if(instance ==null) {   这行代码,不成立。但是线程A new对象的过程还没完成
4. 线程B直接返回instance,但是最终返回的还是一个空的对象,这就造成了线程安全问题。

解决方案就是使用 volatile 禁止JVM的指令重排

双端检索机制 + volatile 关键字

public class Instance {
	private volatile static Instance  instance;
	// 私有化构造方法
	private Instance () {
	}
	// 获取 实例的 方法
	public static Instance getInstance() {
		if(instance == null) {
			// 先判断一次,在进行加锁,比直接加锁,效率要更高。
			synchronized(Instance.class){
				if(instance == null) {
					instance = new Instance();
				}
			}	
		}
		return instance;
	}
}

单例模式的其他实现方式

静态内部类方式

public class Instance {
	// 私有化构造方法
	private Instance () {
	}
	// 获取 实例的 方法
	public staic final Instance getInstance() {
		return InstanceInner.INSTANCE;
	}
	// 私有静态内部类
	private static class InstanceInner {
		private  static final Instance INSTANCE = new Instance();
	}
}

需要注意的是,以上的方式,都可以通过反序列化或反射的方式来进行实例化对象。所以某种长度上,缺乏安全性。

枚举方式

public enum Instance {
    INSTANCE;
    public Instance getInstance(){
        return INSTANCE;
    }
}

枚举方式实现单例模式的优点

1. 能够防止反序列化重新创建对象。
2. 能够避免通过反射方式攻击单例模式。
3. 能够避免线程安全问题。

单例模式实现方式总结及对比

设计模式系列(二):单例模式,看这篇就够了~_第1张图片

你可能感兴趣的:(设计模式)