高频面试必问设计模式——单例模式

Posted 温文艾尔

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了高频面试必问设计模式——单例模式相关的知识,希望对你有一定的参考价值。

⭐️写在前面


  • 这里是温文艾尔的学习之路
  • 👍如果对你有帮助,给博主一个免费的点赞以示鼓励把QAQ
  • 👋博客主页🎉 温文艾尔的学习小屋
  • ⭐️更多文章👨‍🎓请关注温文艾尔主页📝
  • 🍅文章发布日期:2022.03.28
  • 👋java学习之路!
  • 欢迎各位🔎点赞👍评论收藏⭐️
  • 🎄冲冲冲🎄
  • ⭐️上一篇内容:HashMap夺命14问,你能坚持到第几问?

文章目录


单例模式(Singleton Pattern)是java中最简单的设计模式之一,这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建,这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象

单例模式的实现:

  • 饿汉式:类加载就会导致该单例对象被创建
  • 懒汉式:类加载不会导致该单例对象被创建,而是首次使用该对象时才会创建

1.饿汉式-方式1(静态变量方式)

package day06.pattern.singleton.demo1;

/**饿汉式(类加载时即创建对象)
 * 静态成员变量的方式
 * Description
 * User:
 * Date:
 * Time:
 */
public class Singleton 
    //1.构造私有方法
    private Singleton()

    
    //2.在本类中创建奔雷对象
    private static Singleton instance = new Singleton();

    //提供一个公共的访问方式
    public static Singleton getInstance()
        return instance;
    


测试饿汉式

package day06.pattern.singleton.demo1;

/**
 * Description
 * User:
 * Date:
 * Time:
 */
public class Client 
    public static void main(String[] args) 
        Singleton singleton1 = Singleton.getInstance();
        Singleton singleton2 = Singleton.getInstance();
        System.out.println(singleton1==singleton2);
    

true

优点

  • 不用担心线程安全问题,因为每次类加载都会创建一个

缺点

  • 不管有没有使用该对象实例,由类加载创建的对象一直会占用内存

2.懒汉式-方式1(线程不安全)

package day06.pattern.singleton.demo1;

/**懒汉式方式1
 * 线程不安全
 * Description
 * User:
 * Date:
 * Time:
 */
public class Singleton3 

    //私有构造方法
    private Singleton3()

    
    //声明一个Singleton对象
    private static Singleton3 instance;

    //对外提供访问方式
    public static Singleton3 getInstance()
        if (instance==null)
            //线程1等待,线程2获取到cpu的执行权,也会进入到该判断里面
            instance = new Singleton3();
        
        return instance;
    

测试懒汉式

public class Client 
    public static void main(String[] args) 
        Singleton3 singleton3 = Singleton3.getInstance();
        Singleton3 singleton4 = Singleton3.getInstance();
        System.out.println(singleton3==singleton4);
    

true

以上创建对象的方式虽然是单例但是却是不安全的,如果两个线程同时进入到if (instancenull)这一步,那么依然会创建出两个对象==

3.懒汉式-方式2(线程安全)

改进

    //对外提供访问方式
    public static synchronized Singleton3 getInstance()
        if (instance==null)
            instance = new Singleton3();
        
        return instance;
    

加上synchronized锁

我们来讨论一下懒汉式中加锁的问题,对于getInstance()方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必要让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机,由此也产生了一种新的实现模式:双重检查锁模式

    //对外提供访问方式
    public static synchronized Singleton3 getInstance()
        //第一次判断,如果instance不为null,不仅如此抢锁阶段,直接返回实例
        if (instance==null)
            synchronized (Singleton3.class)
                //抢到锁之后判断为了防止instance初始化之后被其他线程再次进行初始化
                if (instance==null)
                    instance = new Singleton3();
                
            
        
        return instance;
    

双重检查锁模式是一种非常好的单例实现模式,解决了单例性能线程安全问题,上面的双重检查锁模式看上去完美,其实也存在问题,多线程情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。要解决双重检查锁模式带来空指针异常的问题,只需要使用volatile关键字,volatile关键字可以保证可见性和有序性

package day06.pattern.singleton.demo1;

/**懒汉式方式1
 * 线程不安全
 * Description
 * User:
 * Date:
 * Time:
 */
public class Singleton3 

    //私有构造方法
    private Singleton3()

    
    //声明一个Singleton对象
    private static volatile Singleton3 instance;

    //对外提供访问方式
    public static synchronized Singleton3 getInstance()
        //第一次判断,如果instance不为null,不仅如此抢锁阶段,直接返回实例
        if (instance==null)
            synchronized (Singleton3.class)
                //抢到锁之后判断为了防止instance初始化之后被其他线程再次进行初始化
                if (instance==null)
                    instance = new Singleton3();
                
            
        
        return instance;
    

4.懒汉式-方式3(静态内部类)

静态内部类单例模式中实例由内部类创建,由于JVM在加载外部类的过程中,是不会加载静态内部类的,只有内部类的属性/方法被调用时才会被加载,并初始化其静态属性,静态属性由于被static修饰,保证只被实例化一次,并且严格保证实例化顺序

package day06.pattern.singleton.demo1;

/**懒汉式-方式4
 * 静态内部类方试
 * Description
 * User:
 * Date:
 * Time:
 */
public class Singleton4 

    //私有构造方法
    private Singleton4()

    //定义一个静态内部类
    private static class SingletonHolder
        //在内部类中声明并初始化外部类的对象
        private static final Singleton4 instance = new Singleton4();
    



    //外部调用的方法
    public Singleton4 getInstance()
        return SingletonHolder.instance;
    

说明:

第一次加载Singleton类时不会去初始化instance,只有第一次调用getInstence,虚拟机加载SingletonHolder并初始化instance,这样不仅能保证线程安全,也能保证Singleton类的唯一性

总结:

静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式,在没有加任何锁的情况下,保证了多线程下的安全

面试题:单例模式静态内部类为何保证了线程安全?

类加载过程分为加载、连接、初始化,而初始化阶段虚拟机会保证一个类的类构造器()在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化同一个类,只有一个线程会真正进行初始化,而其他线程会被阻塞,
jvm对于每一个类,都会有唯一的一个初始化锁LC对应,初始化也只有一个线程能获得LC锁,这样就保证了类的初始化的线程安全

7.枚举方式

枚举方式属于饿汉式

枚举类实现单例模式是极为推荐的单例实现方法,因为枚举类型是线程安全的,并且只会装载一次,设计者充分利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所有单例实现中唯一一种不会被破坏的单例实现模式

package day06.pattern.singleton.demo1;

/**枚举实现单例模式
 * Description
 * User:
 * Date:
 * Time:
 */
public enum  Singleton5 
    INSTANCE

单例设计模式存在的问题

单例模式除枚举方式外是可以被破坏的,有两种方式

  • 序列化
  • 反射

序列化破坏单例测试

代码测试(Singleton类已实现序列化)

package day06.pattern.singleton.demo2;

import java.io.*;

/**
 * Description
 * User:
 * Date:
 * Time:
 */
public class Client 
    public static void main(String[] args) throws IOException, ClassNotFoundException 
//        writeObject2File();
        readObject2File();
        readObject2File();
    

    //从文件中读取数据(对象)
    public static void readObject2File() throws IOException, ClassNotFoundException 
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("F:\\\\note\\\\a.txt"));
        Singleton singleton = (Singleton) ois.readObject();
        System.out.println(singleton);
        ois.close();
    


    //从文件中写数据(对象)
    public static void writeObject2File() throws IOException 
        //1.获取对象
        Singleton instance = Singleton.getInstance();
        //2.创建对象输出流对象
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("F:\\\\note\\\\a.txt"));
        //3.写对象
        oos.writeObject(instance);
        oos.close();
    

day06.pattern.singleton.demo2.Singleton@4dd8dc3
day06.pattern.singleton.demo2.Singleton@6d03e736

我们发现两次读取的对象不同,证明单例被破坏了

序列化破坏单例模式的解决方案

在Singleton类中添加readResolve()方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新new出来的对象

package day06.pattern.singleton.demo2;

import java.io.Serializable;

/**懒汉式-方式4
 * 静态内部类方试
 * Description
 * User:
 * Date:
 * Time:
 */
public class Singleton4 implements Serializable 

    //私有构造方法
    private Singleton4()

    //定义一个静态内部类
    private static class SingletonHolder
        //在内部类中声明并初始化外部类的对象
        private static final Singleton4 instance = new Singleton4();
    



    //外部调用的方法
    public static Singleton4 getInstance()
        return SingletonHolder.instance;
    

    //当进行反序列化时,会自动调用该方法,将该方法的返回值直接返回
    public Object readResolve()
        return SingletonHolder.instance;
    

测试

day06.pattern.singleton.demo2.Singleton4@4dd8dc3
day06.pattern.singleton.demo2.Singleton4@4dd8dc3

发现序列化返回的对象是单例的

反射破坏单例测试

package day06.pattern.singleton.demo3;

import day06.pattern.singleton.demo2.Singleton;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * Description
 * User:
 * Date:
 * Time:
 */
public class Client 
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException 
        //获取类对象
        Class clazz = Singleton4.class;
        //获取无参构造方法
        Constructor constructor =
                clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton4 instance1 = (Singleton4)constructor.newInstance();
        Singleton4 instance2 = (Singleton4)constructor.newInstance();
        System.out.println(instance1==instance2);
    

false

可见反射破坏了单例模式

反射破坏单例模式的解决方案

设置标记量判断是否创建过对象

package day06.pattern.singleton.demo3;

/**懒汉式-方式4
 * 静态内部类方试
 * Description
 * User:
 * Date:
 * Time:
 */
public class Singleton4 

    private static boolean flag = false;

    //私有构造方法
    private Singleton4()
        synchronized (Singleton4.class)
            //判断flag的值是否为true,如果是true,说明非第一次访问直接抛异常
            if (flag)
                throw new RuntimeException("不能创建多个对象");
            
            flag=true;
        
    

    //定义一个静态内部类
    private static class SingletonHolder
        //在内部类中声明并初始化外部类的对象
        private static final Singleton4 instance = new Singleton4();
    



    //外部调用的方法
    public Singleton4 getInstance()
        return SingletonHolder.instance;
    

以上是关于高频面试必问设计模式——单例模式的主要内容,如果未能解决你的问题,请参考以下文章

面试必问之单例模式!

面试官:Spring MVC 如何保证 Controller 的并发安全性?面试必问。。

java工程师面试高频考点之单例模式

架构师内功心法,经典高频面试的单例模式详解

架构师内功心法,经典高频面试的单例模式详解

面试官:Spring MVC 如何保证 Controller 的并发安全性?面试必问。。