文章目录
基本概念
定义
保证一个类仅有一个实例,并提供一个全局的访问点
类型
创建型
适用场景
确保任何情况下都绝对只有一个实例
比如:
- 单服务情况下的计数器可以用单例,但是集群就需要用共享
- 线程池、连接池
- 配置
优点
- 内存中只有一个实例,减少了内存开销
- 避免对资源的多重占用(比如说文件需要避免重复打开导致同时写)
- 设置全局的访问点,严格控制访问
缺点
- 没有接口,扩展困难,如果要修改,肯定要修改代码
需要注意的事情
- 私有化构造器
- 线程安全(非常重要)
- 延迟加载(非常重要)
- 序列化和反序列化安全的问题
- 反射(防止反射攻击)
相关的设计模式
- 单例模式和工厂模式
- 单例模式和享元模式
1. 懒汉式
/**
* @Classname LazySingleton
* @Description 线程不安全的懒汉式
* @Date 2019/12/22 15:58
* @Author Cheng
*/
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
利用idea线程级别debug触发并发情况下的问题
- 在
getInstance()
中的判断处打断点 - 当线程1进行至断点处,单步进入,当并不让其完成初始化
- 切换至线程2,依然可以通过if判断进入
- 之后继续,两个线程便分别创建了一个实例
Thread-0 singleton.LazySingleton@131c736d
Thread-1 singleton.LazySingleton@3dfb47dd
2. 懒汉式改进–类锁
加锁之后,未获取锁的线程将无法进入接下来的逻辑,状态变成monitor
写法一:
/**
* @Classname LazySingleTon2
* @Description 加上类锁保证线程安全
* @Date 2019/12/22 16:20
* @Author Cheng
*/
public class LazySingleTon2 {
private static LazySingleTon2 instance = null;
private LazySingleTon2() {}
public synchronized static LazySingleTon2 getInstance() {
if (instance == null) {
instance = new LazySingleTon2();
}
return instance;
}
}
写法二:
/**
* @Classname LazySingleTon2
* @Description 加上类锁保证线程安全
* @Date 2019/12/22 16:20
* @Author Cheng
*/
public class LazySingleTon2 {
private static LazySingleTon2 instance = null;
private LazySingleTon2() {}
public static LazySingleTon2 getInstance() {
synchronized (LazySingleTon2.class) {
if (instance == null) {
instance = new LazySingleTon2();
}
}
return instance;
}
}
3. double check
!!重要,double check为什么要这么写,最重要的是为什么需要volatile
在懒汉加锁的方式中,我们保证了线程的安全,但是同样也导致了效率的低下,先分析一下为什么会导致效率低下
当我们调用getInstance()
方法的时候,可能有两种情况
instance
为空,需要创建instance
不为空,不需要创建
在第二种情况当中,也就是不需要创建实例的时候,是不存在线程安全问题的,但是在懒汉加锁的模式种,我们对这种情况依然加了锁,这才导致了效率的低下,毕竟绝大多数的时候是不需要创建实例的
那么,我们的目标就比明确了
- 不需要创建实例的时候不加锁,保证效率
- 需要创建实例的是加锁保证线程安全
然后我们的整个设计思路也很清晰,其实就是在懒汉加锁模式外面又加了一层判断
public static LazyDoubleCheckSingleton getInstance(){
if (instance == null) {
synchronized (LazyDoubleCheckSingleton.class) {
if (instance == null) {
instance = new LazyDoubleCheckSingleton();
}
}
}
return instance;
}
这样一来,其实我们已经实现了最初的构想,但是这其中还有一个坑,这就是我们需要volatile的原因
仔细想一想,在之前的懒汉加锁模式中,其实只有一种方式跑完getInstance
的代码,因为我们在一开始就加了锁,而在上面这段代码中,getInstance
方法只有一部分处于临界区,就是加锁的那部分,所以在并发时,其实有可能同时有两个线程处于getInstance
方法中,这就给接下来的问题埋下了伏笔!!
另一个问题就是关于指令重排序的问题,当我们new
一个对象的时候,代码看起来只有一行,但其实包括了三个步骤:
- 给对象分配内存
- 初始化对象
- 将变量指向分配好的内存地址
其中第2、3两个步骤是可能会被编译器重排序的(出于性能优化的目的),现在来假设这么一种情况:
- 指令2、3被重排序了
- 线程1运行完“将变量指向分配好的内存地址 ”,但此时对象尚未初始化完成
- 线程2运行到了外层的
if(instance == null)
,此时变量已经有了引用地址,所以null
判断为false
,将直接返回instance
实例 - 程序调用了实例,但是其实这个实例并没有初始化完成,所以将触发异常
相应的,我们有两种解决方案:
- 禁止重排序
- 重排序对其余线程不可见
而volatile
就是第一种解决方案的实现策略————禁止指令重排序
4. 静态内部类
/**
* @Classname StaticInnerClassSingleton
* @Description 静态内部类实现的单例模式
* @Date 2019/12/22 17:56
* @Author Cheng
*/
public class StaticInnerClassSingleton {
private static class InnerClass{
private static StaticInnerClassSingleton instance = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance() {
return InnerClass.instance;
}
}
首先了解这么一个情况:JVM在初始化类的时候会获取一个锁,来避免多个线程对同一个类的重复初始化,这里还需要了解一个情况,我们的innerClass
定义了一个内部类,但是并没有任何变量指向该类,所以在调用getInstance
方法之前,innerClass
其实是并没有被初始化的。
这样一来,非构造线程就无法看到指令的重排序,那我们的目的也就达到了
附:类(接口)被初始化的条件:
- 类实例被创建
- 类中声明的一个静态方法被调用
- 类中声明的一个静态成员被赋值
- 类中声明的一个静态成员被使用,并且该成员不是一个常量成员
- 类是顶级类,且类中有嵌套的断言语句(不常配到)
5. 饿汉式
缺点就是在类初始化的是就已经创建了实例,一直占用内存,即使没有用到
/**
* @Classname HungrySingleton
* @Description 饿汉式单例模式
* @Date 2019/12/22 18:11
* @Author Cheng
*/
public class HungrySingleton {
// final修饰的变量必须在类初始化完成时已经赋值,所以懒汉式是不能加final的
private final static HungrySingleton instance = new HungrySingleton();
// 也可以用静态代码块赋值
// static{
// instance = new HungrySingleton();
// }
private HungrySingleton() {}
public static HungrySingleton getInstance() {
return instance;
}
}
6. Enum枚举类单例模式
枚举类的单例模式可能是最好的实现模式,经过序列化和反序列化之后的对象仍然是同一个实例,而且其成员变量也是相等的
/**
* @Classname EnumInstance
* @Description 枚举类的单例模式
* @Date 2019/12/22 19:18
* @Author Cheng
*/
public enum EnumInstance {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumInstance getInstance() {
return INSTANCE;
}
}
反序列化对枚举类的处理:readEnum
- 反序列化的时候对于枚举常量,根本就不会创建新的对象,所以对枚举类单例模式根本没影响
反射对枚举类的影响
- 获取不了无参构造器,enum类只有一个构造器,还是需要两个参数的
- 不能反射创建枚举对象,因为newInstance方法会判断是否是枚举类,如果是就抛异常了
- 反编译发现enum是类似于饿汉式的,没有延时初始化
7. 单例模式的破坏:反序列化之后的实例和原来的实例相同吗
我们获取一个实例,然后将该实例序列化到文件中,之后在从文件中反序列化,之后会发现,新的实例和原来的实例不相等,这就违背了单例模式的设计初衷——在任何情况下都只有一个实例
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
HungrySingleton instance = HungrySingleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singletonFile"));
oos.writeObject(instance);
File file = new File("singletonFile");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
HungrySingleton newInstance = (HungrySingleton) ois.readObject();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance==newInstance);
}
}
输出:
singleton.HungrySingleton@14ae5a5
singleton.HungrySingleton@6d03e736
false
解决方案
非常简单,只需要在单例类的代码中添加如下代码:
private Object readResolve(){
return instance;
}
原理
- 反序列化的源码中是通过反射获取的新对象,所以与我们之前的实例不同
- 方法名也是在源码里写明的,有一处就是判断是否实现了
readResolve
方法 - 在这个过程中,虽然返回的是同一个单例,但是确实也创建了一个新的对象
8. 单例模式反射攻击
原理:可以通过反射调用构造器**
public static void main(String[] args){
Class objectClass = HungrySingleton.class;
Constructor constructor = objectClass.getDeclaredConstructor();
// 将私有化的构造器打开访问权限
constructor.setAccessible(true);
HungrySingleton instance = HungrySingleton.getInstance();
HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance==newInstance);
}
显然两个实例时不相等的
解决办法
对于静态内部类的单例模式以及饿汉式单例模式,他们都是在类初始化的时候就把实例创建好了,对此,我们可以在构造器中做一些处理来进行防御
private HungrySingleton() {
if(instance!=null)
throw new RuntimeException("单例构造器禁止反射调用");
}
而对于在调用getInstance
方法时才创建实例的形式来说,情况就有些复杂
- 如果先是正常线程调用,然后是反射线程调用,那么之前的防御性代码依然有效
- 如果先是反射线程调用,然后是正常线程调用,则会产生不同的实例
对此,我们可能会考虑加一些标志位之类的做一些处理,但是,反射是基本可以修改一切的,所以,对于这种lazy-load类型的单例模式,只要反射线程先进入,那就只能创建多个实例了,毕竟防不胜防啊~~
9. 容器单例
- 适合程序初始化时候实例化多个单例对象,多线程情况下是由隐患的,即使是ConcurrentHashmap也是有隐患的
- hashtable是可以的,但是性能较差
- 所以需要根据业务来判断是否用,找一个平衡
/**
* @Classname ContainerSingleton
* @Description TODO
* @Date 2019/12/22 19:38
* @Author Cheng
*/
public class ContainerSingleton {
private static Map<String, Object> sigletonMap = new HashMap<>();
public static void putInstance(String key, Object instance) {
if (key != null && key.length() >=0 && instance != null) {
if (!sigletonMap.containsKey(key)) {
sigletonMap.put(key, instance);
}
}
}
public static Object getInstance(String key) {
return sigletonMap.get(key);
}
}
10. ThreadLocal线程单例模式
- 只保证同一个线程会一直拿到同一个实例
/**
* @Classname ThreadLocalInstance
* @Description TODO
* @Date 2019/12/22 19:50
* @Author Cheng
*/
public class ThreadLocalInstance {
private static final ThreadLocal<ThreadLocalInstance> threadLocalInstanceThreadLocal
= new ThreadLocal(){
@Override
protected Object initialValue() {
return new ThreadLocalInstance();
}
};
private ThreadLocalInstance() {
}
public static ThreadLocalInstance getInstance() {
return threadLocalInstanceThreadLocal.get();
}
}
11. 单例模式的一些应用
- spring中的
AbstractFactoryBean
中的getObject
方法 - MyBatis中的
ErrorContext
的instance
方法用的就是ThreadLocal
方式让每个线程维护自己的上下文
来源:CSDN
作者:SonnSei
链接:https://blog.csdn.net/weixin_40602200/article/details/103653787