面试官:单例模式这么重要,你敢说你不会

我是清风~,每天学习一点点,快乐成长多一点,这些都是我的日常笔记以及总结。

目录

  • 单例模式
    • 1、懒汉式单例模式
      • 未初始化问题解决
        • Double Check 双重检查
        • 方案一:不让第二步和第三步重排序-DoubleCheck
        • 方案二:基于类初始化-静态内部类
    • 2、饿汉式
      • 饿汉式与懒汉式最大区别
    • 3、序列化破坏单例模式原理
    • 4、枚举单例
    • 5、基于容器的单例模式
    • 6、基于TreadLocal线程单例
    • 7、源码分析-JDK
    • 8、源码分析-spring
    • 其他相关模式

单例模式

  • 定义:保证一个类仅有一个实例,并提供一个全局访问点
  • 类型:创建型

适用场景

  1. 想确保任何情况下都绝对只有一个实例
    在单服务的情况下,网站的接收器可以用单例,在集群情况下就要用共享的接收器了
    还有一些应用配置,一般线程池的时候也使用单例数据库的连接池也是单例模式
    单例模式在日常项目中也是使用最广泛的,面试时候,单例模式也是高频考点

优点

  1. 在内存里只有一个实例,减少了内存开销
    特别是一个对象需要频繁的创建销毁时,而且创建销毁时的性能还无法优化,这个单例模式优势很明显。
  2. 可以避免对资源的多重占用
    例如对一个文件写操作,由于只有一个实例存在内存中,可以对同一个资源文件的同时写操作。
  3. 设置全局访问点,严格控制访问
    就是对外不让你new出来,只能通过我这个方法来创建单例对象,严格控制访问。

缺点

  • 没有接口,扩展困难

重点

  1. 私有构造器
    就是为了禁止从单例内外部调用构造函数,来创建这个对象。为了到达这个目的必须设置构造函数,权限设置为private。
  2. 线程安全(非常重要的)
  3. 延迟加载
    在使用它的时候在创建
  4. 序列化和反序列化安全
  5. 反射

单例-Double Check

1、懒汉式单例模式

注重延迟加载,只有使用它的时候,才会初始化,不使用就不会加载LazySingleton对象

synchronized 这里的关键字可以防止两个拿到不是同一对象
通过这种同步的方式,解决了懒汉式在多线程可能引起的问题
但是我们也知道同步锁synchronized也比较消耗资源,有加锁和解锁的开销,
而且synchronized修饰static时候,锁的是class,这个锁范围是很大的,对性能有一定的影响。

public class LazySingleton {
    /**首先申明静态对象**/
    private static LazySingleton lazySingleton = null;
    //懒汉式就是比较懒,初始化的时候是没有被创建的
    //延迟加载,构造器是private
    private LazySingleton(){
        if(lazySingleton != null){
            throw new RuntimeException("单例构造器禁止反射调用");
        }
    }
    /**获取LazySingleton的方法**/
    //不加synchronized时候,单线程是可以的
    //但是多线程来使用单例的话
    //lazySingleton = new LazySingleton();
    // 这里的lazySingleton会被new两次
    public synchronized static LazySingleton getInstance(){
        //空判断
        if(lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
 }
public class T implements Runnable {
    @Override
    public void run() {
        LazySingleton lazySingleton = LazySingleton.getInstance();
        System.out.println(Thread.currentThread().getName()+"  "+lazySingleton);
    }
}
public class Test {
    public static void main(String[] args) {
        Thread t1 = new Thread(new T());
        Thread t2 = new Thread(new T());
        t1.start();
        t2.start();
        System.out.println("program end");
    }
}
输出结果
program end
Thread-0  com.qingfeng.singleton.LazySingleton@710a3057
Thread-1  com.qingfeng.singleton.LazySingleton@710a3057

未初始化问题解决

Double Check 双重检查

懒汉式,这种方式兼顾了性能和性能安全
它是在哪里检查呐?
首先方法不用锁了,而是把锁定放到了方法体中。
所有还是先进行判断,判断完成之后,我们把锁定这个单例的类

public class LazyDoubleCheckSingleton {
    private  static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
    private LazyDoubleCheckSingleton(){

    }
    public static LazyDoubleCheckSingleton getInstance(){
        if(lazyDoubleCheckSingleton == null){
            //锁定这个类
            synchronized (LazyDoubleCheckSingleton.class){
                //加锁之后,还要做一层空的判断
                //不如不加锁,不给返回,不为null也只有一个线程进去
                if(lazyDoubleCheckSingleton == null){
                    lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                    //这里有个隐患,这里有可能还没有初始化们,这里经历了三个步骤
                    //1.分配内存给这个对象
//                  //3.设置lazyDoubleCheckSingleton 指向刚分配的内存地址
                    //2.初始化对象(出现重排序,颠倒了)
//                    intra-thread semantics互换位置不会改变单线程内的重排结果
//                    ---------------//3.设置lazyDoubleCheckSingleton 指向刚分配的内存地址
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
}

演示的是单线程情况
首先从上到下,是线程执行的时间,
首先分配对象内存空间
第二步,正常应该是初始化对象
第三步,设置lazyDoubleCheckSingleton 指向内存空间(lazyDoubleCheckSingleton为instance)
(所以2和3怎么换顺序,第四步都是在最后)
2和3的重排序对结果没有影响
面试官:单例模式这么重要,你敢说你不会_第1张图片
下面是多线程演示
时间还是从上之下,
【线程0】在左侧,右侧是【线程1】
第一步,先分配内存空间
假设【线程0】重排了,先设置了单例对象指向内存空间,
这个时候【线程1】判断instance是否为null,这个时候判断出来,instance并不为null
因为它有指向内存空间
然后【线程1】开始访问内存对象。就说线程1比线程0更早的访问对象
所以【线程1】访问到的对象是在线程0中还没有初始化完成的对象,这个时候就有问题了

这个对象并没有完整的被初始化,系统要报异常
对于第二步和第三步重排序,并不影响第四步步骤
面试官:单例模式这么重要,你敢说你不会_第2张图片

解决:
方案一:可以不让第二步和第三步重排序
方案儿允许线程0里面的2和3重排序,但是不能允许线程1看到这个重排序

方案一:不让第二步和第三步重排序-DoubleCheck

代码实现
加一个关键字volatile实现线程安全的初始化
加了volatile之后,所有线程都能看见共享内存的这些状态,保证了内存的可见性
在volatile修饰的共享变量,在进行写操作的时候,会多一些汇编代码

起到两个作用

  1. 当前处理群缓存行的数据写回到系统内存。这个写内存的操作呢,会使其他cpu里面缓存了该内存地址的无效。因为其他cpu缓存无效了,那么又从共享内存同步数据,这样就保证了内存的可见性
  2. 这里面主要使用缓存一直性协议,当处理器发现我这个缓存无效了,我在重新进行操作的时候,会从系统内存种把数据读到处理群的缓存里
public class LazyDoubleCheckSingleton {
    private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
    private LazyDoubleCheckSingleton(){

    }
    public static LazyDoubleCheckSingleton getInstance(){
        if(lazyDoubleCheckSingleton == null){
            //锁定这个类
            synchronized (LazyDoubleCheckSingleton.class){
                //加锁之后,还要做一层空的判断
                //不如不加锁,不给返回,不为null也只有一个线程进去
                if(lazyDoubleCheckSingleton == null){
                    lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
}

方案二:基于类初始化-静态内部类

这个解决方案是通过静态内部类解决
权限要声明为private

public class StaticInnerClassSingleton {
    //权限要声明为private
    private static class InnerClass{
        private static StaticInnerClassSingleton staticInnerClassSingleton
                = new StaticInnerClassSingleton();
    }
    //对外开放这个方法
    public static StaticInnerClassSingleton getInstance(){
        return InnerClass.staticInnerClassSingleton;
    }
    private StaticInnerClassSingleton(){
        if(InnerClass.staticInnerClassSingleton != null){
            throw new RuntimeException("单例构造器禁止反射调用");
        }
    }
}

JVM在类的初始化阶段,也就是class被加载后,并且被线程使用之前,都是类 的初始化阶段。
在这个阶段会执行类的初始化
在执行类的初始化期间,JVM会获取一个锁,这个锁可以同步多个线程对一个类的初始化(就说绿色部分)
基于这个特性,可以实现基于静态内部类的,并且线程安全的延迟初始化方案

对于步骤2和步骤3,线程1并不会看到,就是说非构造线程是不允许看到这个重排序的
因为之前是【线程0】来构造这个单例对象

初始化一个类,包括执行这个类的静态初始化,还有初始化种,在这个类申明的静态变量

根据Java语言规范,主要分为五种情况

  • 首次发生的时候,一个类将被立刻初始化。这里所说的类是泛指,包括interface也是一个类
    假设这个类是A,说一下分几种情况都会导致整个A类都会立刻被初始化
  1. 第一种,有一个A类型的实例被创建
  2. 第二种,A类中声明一个静态方法被调用
  3. 第三种,A类中一个静态成员被赋值
  4. 第四种,A类中声明的静态成员被使用,并且这个成员还不是一个常量成员
  5. 第五种,A类是一个顶级类,其他也有断延语句
    前四种,实际工作用的比较多
    只要首次碰到上述某一个情况,这个类都会马上立刻被初始化
    面试官:单例模式这么重要,你敢说你不会_第3张图片
    下图看出,【线程0】和【线程1】试图来获取这个锁的时候,也就是class对象初始化锁,这个时候肯定只有一个线程获得锁。
    假设【线程0】获得这个锁了,【线程0】执行静态内部类的初始化,对于静态内部类,即使步骤2和步骤3之间出现重排序。
    但是**【线程1】是无法看到重排序的,因为这里有一个class对象的初始化锁**
    因为这里面有锁,,对于【线程0】而言,初始化静态内部类的时候,就说把instance从中new出来,所有步骤2和步骤3怎么排序无所谓,【线程1】看不到,因为线程在等待
    面试官:单例模式这么重要,你敢说你不会_第4张图片
    静态内部类和Double Check都是为做延迟初始化,来降低创建单例实例的开销
    具体用什么方案,要看,单例对象是什么样子的

关于面试方面:单例模式在面试过程非常重要,一层一层迭代讲解

2、饿汉式

最简单的实例化
在类加载的时候就完成实例化
优点:

  • 写法简单,类加载的时候就完成了初始化,避免了同步问题

缺点:

  • 也是类同步的时候完成初始化,没有延迟加载的效果。
  • 如果整个类从始至终,我们系统都没有用过,还会造成内存的浪费
public class HungrySingleton implements Serializable,Cloneable{
    //这样对象就不可改了,因为在类加载的时候就初始化好了
    private final static HungrySingleton hungrySingleton = new hungrySingleton();
    private HungrySingleton(){
    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

也可以把单例实例化过程放到静态代码块里面

public class HungrySingleton implements Serializable,Cloneable{
    //这样对象就不可改了,因为在类加载的时候就初始化好了
    private final static HungrySingleton hungrySingleton;

    static{
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton(){
        }
    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

饿汉式与懒汉式最大区别

延迟加载
类加载的时候初始化——饿汉式
调用之后在初始化——懒汉式

有什么技巧记忆
饿汉式,很饿,一上来就要吃东西,一上来就把对象new好了
懒汉式,比较懒,你不用它的时候,他都不会创建对象

3、序列化破坏单例模式原理

public class Test {
    public static void main(String[] args) {
        HungrySingleton instance = HungrySingleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
        oos.writeObject(instance);
        File file = new File("singleton_file");
        
        //读取ObjectInputStream,跟上面声明的做对比,是不是同一个对象
        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);
        }
   }
输出结果
com.qingfeng.singleton.HungrySingleton@312b1dae
com.qingfeng.singleton.HungrySingleton@5a2e4553
false

看到instance这个对象312b1dae和
newInstance是5a2e4553,他们是不相等的
这就违背了单例模式的初衷
通过序列化和反序列拿到了不同的对象

解法在单例类里面写一个方法

public class HungrySingleton implements Serializable,Cloneable{
    //这样对象就不可改了,因为在类加载的时候就初始化好了
    private final static HungrySingleton hungrySingleton;

    static{
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton(){
    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }

    private Object readResolve(){
        return hungrySingleton;
    }

}

在运行测试test

输出结果
com.qingfeng.singleton.HungrySingleton@312b1dae
com.qingfeng.singleton.HungrySingleton@312b1dae
true

这里结果就相等了,肯定会存在疑问,为什么在单例里面加上readResolve就可以了
用的反射
面试官:单例模式这么重要,你敢说你不会_第5张图片
反射攻击解决方案
通过反射,打开构造器的权限,然后获取这个对象
通过权限将构造器设置为ture,这个单例里面的构造器private权限就放开了
对象就能实例化出来

public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException, 				                        
                       NoSuchMethodException, IllegalAccessException, InvocationTargetException,   
                       InstantiationException {
        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);      
    }
}

输出显示

com.qingfeng.HungrySingleton@12edcd21
com.qingfeng.HungrySingleton@34c45dca
false

4、枚举单例

在多线程的时候是没有问题的
枚举类天然的可序列化机制,能够强有力的保证不会出现多次实例化情况
枚举类的单例可能是单例模式中最佳实践

public enum EnumInstance {
    INSTANCE{
        protected  void printTest(){
            System.out.println("qingfeng Print Test");
        }
    };
    protected abstract void printTest();
    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
    public static EnumInstance getInstance(){
        return INSTANCE;
    }
}

5、基于容器的单例模式

容器单例就是
通过map来实现一个单例对象的容器
这里面保证key的合法性和唯一性
这种写法,非常适合在程序初始化的适合
我们把多个单例对象放入到singletonMap统一管理
使用的时候,通过key直接map当中获取单例对象

public class ContainerSingleton {

    private ContainerSingleton(){
    }
    private static Map<String,Object> singletonMap = new HashMap<String,Object>();

    public static void putInstance(String key,Object instance){
        if(StringUtils.isNotBlank(key) && instance != null){
            if(!singletonMap.containsKey(key)){
                singletonMap.put(key,instance);
            }
        } 
    }
    public static Object getInstance(String key){
        return singletonMap.get(key);
    }
}

6、基于TreadLocal线程单例

这个单例不能保证整个应用全局唯一,但是可以保证线程唯一
TreadLocal隔离多个线程,对数据的访问冲突
对于多线程共享问题,如果我们使用同步锁,其实就是以时间和空间的方式,因为要排队
要是 TreadLocal,就是用空间换时间的方式,他会创建很多对象,至少在一个线程里创建一个
但是对于这个线程,他获取对象是唯一的

public class ThreadLocalInstance {
    private static final ThreadLocal<ThreadLocalInstance> threadLocalInstanceThreadLocal
             = new ThreadLocal<ThreadLocalInstance>(){
         //重写初始化方法
        @Override
        protected ThreadLocalInstance initialValue() {
            return new ThreadLocalInstance();
        }
    };
    private ThreadLocalInstance(){

    }

    public static ThreadLocalInstance getInstance(){
        return threadLocalInstanceThreadLocal.get();
    }
}

7、源码分析-JDK

ctrl+n 在Runtime下面
面试官:单例模式这么重要,你敢说你不会_第6张图片
这里直接返回了currentRuntime
面试官:单例模式这么重要,你敢说你不会_第7张图片
进到返回currentRuntime
这个对象是一个static,并且在类加载的时候就创建出来了,属于饿汉式
面试官:单例模式这么重要,你敢说你不会_第8张图片

8、源码分析-spring

spring的单例是基于容器的
如果写一个主函数获得一个应用,启动了多个容器,在每个容器都能达到这个单例对象
spring单例是bean作用域中的一个,
这个作用域在每一个应用程序上下文中仅创建一个,我们设置应该单例的属性的实例

和设计模式最大区别

  1. spring将实例的数量限制的作用域在整个程序的上下文
  2. 而我们写的单例模式,在java运用实例中,将这些实例数量限制在给定的类加载管理的整个空间里

所有spring容器即使是单例,也可以拿出来使用
spring中也可以找到单例的影子
进去,后可以发现这里做了一些判断
AbstractFactoryBean
面试官:单例模式这么重要,你敢说你不会_第9张图片
如果整个对象是单例,
就执行return程序,,可以看到是否已经初始化,初始化了直接返回,
如果没有初始化,调用getEarlySingletonInstance方法
否则不是单例直接创建instance
面试官:单例模式这么重要,你敢说你不会_第10张图片

其他相关模式

[Java设计模式] 建造者模式
[Java设计模式] 抽象工厂—产品等级结构与产品族
[Java设计模式] 简单工厂和工厂方法

你可能感兴趣的:(java面试,多线程,java,设计模式,面试,spring)