看了这篇文章,你能和面试官畅谈单例模式

一、前言

最近看了很多的书还有视频,他们都花了很长的篇幅提到了单例模式,于是我想把他们都总结起来,写下这篇文章。目的就是,让小白能搞懂单例模式,以及==单例模式的经典面试题==。为什么说是小白也能懂的呢?哈哈哈,还不是小胖也是一个小白~~~

二、单例模式的解释

单例模式定义:一个类只能有一个实例,且该类能自行创建这个实例的一种模式。其实单例模式在C#或者.NET里面更好理解,像win7的任务管理器,在系统中只能创建一个。有些理解了嘛?

单例模式只能有一个实例,实例化其实就是new的过程,是不可能阻止他人不去用new的。所以我们完全可以直接就把这个类的构造方法改成私有的。对于外部的代码,不能用new来实例化他,我们完全可以再写一个public方法,叫做getInstance(),这个方法的目的就是返回一个实例,但是在这个方法中,我们需要是否实例化的判断。

来一个简单的例子
package singleton;

/**
 * 描述: 懒汉式(线程不安全)
 * **/
public class Singleton3 {
    private static Singleton3 instance;

    private Singleton3(){

    }
    //如果两个线程同时到达,会出现线程不安全的情况
    public static Singleton3 getInstance(){
        if (instance == null){
            instance = new Singleton3();
        }
        return instance;
    }
}

单例模式,保证一个类仅有一个实例,并提供一个访问它的全局访问点。可以节省内存和计算、保证结果正确、方便管理。适用场景是无状态的工具类、全局信息类。

==记住,上面的单例模式是线程不安全的。==

三、实现单例模式的8种写法

1.饿汉式(静态常量)(可用)
package singleton;

/***
 * 描述:饿汉式(静态常量)  (可用)
 * **/
public class Singleton1  {
    private final static Singleton1 INSTANCE = new Singleton1();

    private Singleton1(){

    }
    public static Singleton1 getInstance(){
        return INSTANCE;
    }
}

上面是饿汉式的静态常量的写法,可以看到类在加载后就完成了实例化的创建。优点:写法简单,类加载后就完成了实例的创建。缺点:提前占用系统的资源。

2.饿汉式(静态代码块)(可用)
package singleton;

/***
 * 描述:饿汉式(静态代码块)  (可用)
 * **/
public class Singleton2  {
    private final static Singleton2 INSTANCE;

    static {
        INSTANCE = new Singleton2();
    }

    private Singleton2(){

    }
    public static Singleton2 getInstance(){
        return INSTANCE;
    }
}

上面是饿汉式的静态代码块的方式,只不过和第一种有一些区别。优缺点和第一种是一样的。优点:写法简单,类加载后就完成了实例的创建。缺点:提前占用系统的资源。

3.懒汉式(线程不安全)(不可用)
package singleton;

/**
 * 描述: 懒汉式(线程不安全)
 * **/
public class Singleton3 {
    private static Singleton3 instance;

    private Singleton3(){

    }
    //如果两个线程同时到达,会出现线程不安全的情况
    public static Singleton3 getInstance(){
        if (instance == null){
            instance = new Singleton3();
        }
        return instance;
    }
}

上面的懒汉式的写法,它是线程不安全的,因为在多线程的时候,可能会有两个线程同时到达instance==null,然后都会初始化,这就创建了两个对象了,是线程不安全的,不能使用。

4.懒汉式(线程安全)(不推荐)
package singleton;

/**
 * 描述:懒汉式(线程安全)(不推荐)
 * */
public class Singleton4 {
    private  static  Singleton4 instance;
    private Singleton4(){

    }
    //但是效率不高
    public synchronized static Singleton4 getInstance(){
        if (instance == null){
            instance = new Singleton4();
        }
        return instance;
    }
}

上面是懒汉式的第二种写法,这种方法是线程安全的,但是不推荐使用,因为效率是低下的。在getInstance上面加上了synchronized的同步方法,那么只能有一个线程可以进入到这个方法中,但是在多线程的时候效率是非常低的,因为任何一个线程进入的这个方法,都需要去等待锁的释放,所以不推荐使用。

5.懒汉式(线程不安全)(不可用)
package singleton;

/**
 * 描述:懒汉式(线程不安全) (不推荐)
 * */
public class Singleton5 {
    private static Singleton5 instance;

    private Singleton5(){

    }
    public static Singleton5 getInstance(){
        if (instance == null){
            synchronized (Singleton5.class){
                instance = new Singleton5();
            }
        }
        return instance;
    }
}

上面是懒汉式的第三种写法,是对第二种的改进,不在锁在方法上,加在创建对象上面,但是这可能会引发线程安全的问题。如果连个线程都判断instance==null,都进入到if里面,这时只有一个线程会运行,但是第一个线程执行完成之后,第二个线程还是会创建实例,那么就是线程不安全的。

6.懒汉式(双重检查)(推荐面试使用)
package singleton;

/**
 * 描述: 双重检查
 * 优点: 线程安全;延迟加载;效率较高
 * 为什么要double-check
 * 1.线程安全
 * 2.单check为什么不行?
 * 3.放在判断后面会引发线程安全问题
 * 4.单层锁,但是synchronized放在方法上,这样可以,但是会导致性能问题
 *
 * 为什么要用volatile
 * 1.新建对象实际上有3个步骤(分配内存资源,调用构造函数,将对象指向分配的内存空间)新建对象不是原子操作。
 * 2.JVM重排序会带来NPE(空指针的问题)
 * 3.防止重排序
 * */
public class Singleton6 {
    private volatile static Singleton6 instance;
    private Singleton6(){

    }
    public static Singleton6 getInstance(){
        if (instance == null){
            synchronized (Singleton6.class){
                if (instance == null){
                    instance = new Singleton6();
                }
            }
        }
        return instance;
    }
}

上面就是很有名的double-check单例模式了,它的优点有:线程安全;延迟加载;效率较高。它是线程安全的,而且效率很高,推荐我们在面试的时候使用。这个代码同时也引出了我们在面试过程中的2个问题。
==懒汉式单例模式为什么要用double-check,不用就不安全吗?懒汉式单例模式为什么双重检查模式要用volatile?==

7.懒汉(静态内部类方式)(可用)
package singleton;

/**
 * 描述: 静态内部类方式,可用
 * 懒汉
 * ***/
public class Singleton7 {
    private Singleton7(){
    }
    private static class SingletonInstance{
        private static final Singleton7 INSTANCE = new Singleton7();
    }
    public static Singleton7 getInstance(){
        return SingletonInstance.INSTANCE;
    }
}

上面是静态内部类的方式,是可以推荐使用的,而且效率也是可以的。外部类加载,JVM不会创建多个实例。

8.枚举 推荐在项目中使用
package singleton;

/**
 * 描述:单例模式:枚举 推荐用
 * */
public enum Singleton8 {
    INSTANCE;

    public void whatever(){
    //无论什么方法
    }
}

上面是枚举方式的单例模式,是生产实践中最佳的单例模式的写法,同时可以防止反序列化破坏单例。

四、常见面试问题

什么叫单例设计模式

答:单例模式的重点在于整个系统上共享一些创建时较耗资源的对象。整个应用只维护一个特定类实例,它被所有组件共同使用。Java.lang.Runtime是单例模式的经典例子。

你知道饿汉式的缺点吗?

答:饿汉模式,类一加载的时候就会实例化对象,所以要提前占用系统资源。

那懒汉式的缺点呢?

答:不会出现占用资源的问题,但是需要使用合适,否则会带来线程安全问题。

懒汉式单例模式为什么要用double-check,不用就不安全吗?

答:为了线程安全,我们需要使用double-check。

追问,单check为什么不行?(代码见5.懒汉式)

答:单check是线程不安全的(代码见5.懒汉式),可能会有多个线程走到了 if (instance == null)的里面,由于synchronized看似只能有一个线程会创建对象,但是第二个也会创建。

追问,你可以把synchronized写在方法的外面呀?(代码见4,。懒汉式)

答:这个是可以解决线程安全的问题,但是效率不是很高,每个线程都需要等待锁的释放,会导致性能的问题,不推荐使用。

你说为什么要用volatile?

答:在多线程的时候,创建对象分为3步,CPU可能会重排序,首先建一个空的对象,然后复制给引用,然后调用构造方法。
第一个线程进来了,第一个对象已经不是空的,但是构造方法没有执行,里面的属性是空的。第二个线程发现不是空的,就会直接跳过创建实例的方法,之后再使用的时候引发的问题。使用volatile可以避免这个问题,对于第二个线程来说,他的创建过程对第一个线程来说是可见了,他就会等待创建完成。

那这么多应该如何选择,用哪种单例的实现方案最好?

答:枚举方式的单例模式,是生产实践中最佳的单例模式的写法,同时可以防止反序列化破坏单例。

那你知道happens-before原则嘛?

答:volatile就是happens-before原则呀......==未完待续==

请用Java写出线程安全的单例模式。

答:上面已经很多例子了,小胖觉得可以用第6种double-check。

五、关于几种解法的选择

《剑指offer》上面的推荐给面试官的解法是1.饿汉式(静态常量)(可用)和7.懒汉(静态内部类方式)(可用)。
《大话设计模式》上面的推荐也是1.饿汉式(静态常量)(可用)
《线程八大核心+Java并发底层原理精讲》上面推荐使用6.懒汉式(双重检查)(推荐面试使用)

小胖觉得可以使用第6种,这可能会==打开一些面试的问题==,把问题引入到我们了解熟悉的方向。当然你在手写单例模式的时候,可以去询问一下要求,是需要饿汉式还是懒汉式。

六、参考资料

书籍1:《大话设计模式》 第21章 有些类也需计划生育--单例模式
书籍2:《剑指offer》 面试题2:实现Singleton模式
视频:《线程八大核心+Java并发底层原理精讲》

七、关于本系列的解释

本系列想制作23种设计模式+7种设计原则一系列课程,其目的就是一个简单的记录学习的过程。不知道能帮助到多少人,也不知道技术是否会有一定的深度。

==制作不易,您的点赞是我最大的动力。点赞10个,我会发出下一篇文章==

你可能感兴趣的:(看了这篇文章,你能和面试官畅谈单例模式)