高并发下的单例模式,你真的会了吗?

166 阅读3分钟

知识点

1、模式定义/应用场景/
2、字节码的试试和指令重排序
3、类加载机制
4、序列化机制
5、单例模式在spring框架源码中的应用

一、模式适用的场景

保证一个类只有一个实例,并且提供一个全局的访问点。

适用于: 重量级的对象,不需要多个实例,比如线程池、连接池等

image.png

二、五种单例模式和序列化机制

2.1 懒汉模式:延迟加载,只有在真正适用的时候,才实例化对象

知识点:线程安全问题 1、double-check加锁优化 2、JIT编译优化,指令重排


class LazySingleton{
    private volatile static LazySingleton instance;
    private LazySinglton(){}
    public static LazySingleton getInstance(){
        if(instance == null){
            synchronized(LazySingleton.class){
                // 如果只是加锁,不做第二道判断的话,其他线程进入第一道判断之后,还是拿到锁之后,仍然会实例化对象。
                if(instance == null){
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }
}

为什么需要加上volatile关键字? JVM在实例化对象的时候,可以分为三个部分
1、分配对象的空间
2、实例化对象
3、给引用赋值

由于第二步和第三步没有数据依赖,所以编译器可能会对他们进行重排序,如果重排序后的结果如下
1、分配对象的空间
2、给引用赋值
3、实例化对象

那么线程就有可能拿到没有初始化好的对象。

2.2饿汉模式

在类加载的初始化阶段的时候,就完成了实例的初始化,本质就是借助JVM的类加载机制,保证实例的唯一性(初始化的过程只会执行一次)及线程安全(JVM以同步的方式来完成类加载的整个过程)。

类加载的过程: 1、加载二进制数据到内存中,生成对应的Class数据结构 2、链接:a、验证 b、准备(给类的静态成员变量赋默认值) c、解析 3、初始化:给类的静态变量赋初值

只有真正适用对应的类时,才会触发初始化,如

  • 当前类时启动类(main函数所在类)
  • 直接进行new操作
  • 访问静态属性
  • 访问静态方法
  • 用反射访问类
  • 初始化一个类的子类等
    class HungrySingleton{
        private static HungrySingleton instance = new HungrySingleton();
        private HungrySingleton(){}
        public static HungrySingleton getInstance(){
            return instance;
        }
    }

2.3静态内部类

1、本质上是利用类的加载机制来保证线程安全
2、只有实际使用的时候,才会触发类的初始化,所以也是懒加载的一种形式。


class InnerClassSingleton{
    private static class InnerClassHoder{
        private static InnerClassSingleton instance = new InnerClassSingleton();
        
        private InnerClassSingleton(){};
        
        public static InnerClassSingleton getInstance(){
            return InnerClassHoder.instance;
        }
        
    }
}

2.4使用反射破坏单例

Constructor<InnerClassSingleton> declaredConstructor = InnerClassSingleton.class.getDeclaredConstructor();  
declaredConstructor.setAccessible(true);
InnerClassSingleton innerClassSingleton = declaredConstructor.newInstance();

InnerClassSingleton instance =  InnerClassSingleton.getInstance();

System.out.println(innerClassSingleton == instance);  //false

静态内部类可以防止反射破坏


class InnerClassSingleton{
    private static class InnerClassHoder{
        private static InnerClassSingleton instance = new InnerClassSingleton();
        
        private InnerClassSingleton(){
            if(InnerClassHoder.instance != null){
                throw new RuntimeException("单例不允许有多个实例");
            }
        };
        
        public static InnerClassSingleton getInstance(){
            return InnerClassHoder.instance;
        }
        
    }

2.5枚举类型

1、newInstancede 方法中已经对枚举类型进行判断,无法重复实例化。所以枚举类型天然不支持反射对应的实例,并且有自己的反序列机制。

        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");

2、利用类加载机制保证了线程安全。

2.6序列化

首先我们可以思考一下,我们创建的一个对象,和通过rpc调用(进行了序列化和反序列)之后的对象还是同一个对象吗?
答案是否定的。

我们需要在序列化的时候,实现readResolve方法

Object readResolve() throws ObjectStreamException{
    return InnerClassHolder.instance;
}

Notes: 需要自定义序列化Id,否则会跑一个序列化Id不一致的异常。