单例模式,你真的搞懂了吗

192 阅读5分钟

前言:

大家好,我是Felix。单例在java的面试中,其实属于是被问烂了的话题了,但是为啥呢,就要问这个呢。因为它其实能够考察java相关的知识非常多。我曾经就在拼多多一面中被要求手写单例模式,并口头描述相关原因。那我们今天来缕一缕这个单例模式吧。

  1. 什么是单例模式

在我们的应用中,绝大多数的Bean对象都属于是无状态的对象。无状态:指的就是多个用户线程来访问这个Bean, 返回同一个bean对象,对于业务没有任何影响。所以说,我们应该对外提供的这类bean就只给一个,这样,大大节省了我们的内存空间,应用也是比较高效的。

其实,我们在学习spring框架的时候,就学习过bean的作用域,常见的就有singleTon和protocal,我们大多数也是采用单例的。由于spring帮我们管理好了这些bean,所以很多时候我们不太需要自己手写单例模式了。

PS: 在Java中,单例模式一般遵循这种编码规范

- 私有化构造方法(不允许别人去new 我这个对象)
- 对外提供一个方法,返回bean

2. 单例模式的分类

主要分为两类,懒汉式与饿汉式,下面我先大体的描述一下两种,后续的代码中再去完善。

1. 饿汉式

可以理解为是一个勤劳的人,很喜欢未雨绸缪,在类初始化的时候就已经完成了bean的创建了,看如下代码,也很好理解。

/**
 * 饿汉式
 */
public class EHan {

    //类在初始化的时候,bean已经创建好了
    private static EHan instance = new EHan();

    private EHan() {

    }

    public static EHan getInstance() {
        return instance;
    }
}

好处:应用起来后,bean已经创建好了,保证了运行时候的性能,也不会有线程安全相关的问题

坏处:不能保证每个bean在应用期间都会被用到,可能造成内存的浪费,其实就是内存泄露问题。

2. 懒汉式

可以理解为一个懒惰的人,就是拖延症了,可以说是鼻涕不到嘴边不擦,哈哈哈,所以只有真的有人用我才来给你创建。可以看如下代码。

/**
 * 懒汉式
 */
public class LHan {

    //类加载的时候,bean其实式空的
    private static LHan instance;

    //被调用时候才会创建
    public static LHan getInstance() {
        if (null == instance) {
            instance = new LHan();
        }

        return instance;
    }

    private LHan() {

    }
}

好处:用到什么,创建什么,对于内存很友好

坏处:高并发的情况下,很多时候就不一定式单例了,而且还有可能是null,一下会详细说明

  1. 懒汉式单例存在的问题,如何去解决呢

很多时候,内存是非常珍贵的资源,所以一般我们都是采用懒汉式的单例模式。但是,懒汉式的会有啥问题,如何解决呢?

1. 高并发情况下会有多个bean产生

如下图,想象一下,如果两个线程同时到达第13行代码,此时,由于bean是null,所以都会去创建,那如果是秒杀场景下呢,成千上万的线程就会过来,那内存很有可能极大的浪费了,系统性能也会下降。

如何解决呢?理所当然我们会想到锁呀,看代码

/**
 * 懒汉式
 */
public class LHan {

    private static LHan instance;

    public static LHan getInstance() {
        //对于进来的第一个线程,拿到锁了再去实例化
        synchronized (LHan.class) {
            if (null == instance) {
                instance = new LHan();
            }
        }
        return instance;
    }

    private LHan() {

    }
}
2. 影响性能

这个确实可以解决,但是,还是有问题的呢。因为bean的初始化确实满足单例,但是后续的使用,有没有发现每次都要上锁,这个性能影响太大了。继续优化如下。

/**
 * 懒汉式
 */
public class LHan {


    private static LHan instance;

    public static LHan getInstance() {
        //这里,如果说我的bean没有创建,那我才去加锁创建,否则,直接返回
        if (null == instance) {
            synchronized (LHan.class) {
                if (null == instance) {
                    instance = new LHan();
                }
            }
        }
        return instance;
    }

    private LHan() {

    }
}

小结: 其实单例模式写到这里,代码层面已经没有问题了,但是还是有坑。我说这个bean有可能拿到的是null,你相信嘛?

3. 指令重排会引起bean为null
    * 什么是指令重排

指令重排是指,编译器或者处理器为了优化性能,会将计算机的指令进行重新排序,并行执行。可以这样简单的理解。

单线程模式下,指令重排不会对我们的代码有什么影响,但是多线程下就会有问题。

    * Bean的创建过程
        + 实例化:就是去堆中开辟空间
        + 初始化:给bean中的属性进行赋值
        + 暴露引用:把地址值赋值给我们的变量,在这里就是instance

由于指令重排的存在,第二步,第三步很有可能就是反的,那这样,我们的bean其实就是未初始化的一个bean了。

那如何解决呢?

答案:使用java的关键字,volatile

它的作用就是禁止指令重排,使得第三步一定是在第二步后面。那我们看看最终的代码吧。

/**
 * 懒汉式
 */
public class LHan {

    //这里,加上了volatile关键字!
    private static volatile LHan instance;

    public static LHan getInstance() {
        //这里,如果说我的bean没有创建,那我才去加锁创建,否则,直接返回
        if (null == instance) {
            synchronized (LHan.class) {
                if (null == instance) {
                    instance = new LHan();
                }
            }
        }
        return instance;
    }

    private LHan() {

    }
}

总结:没想到一个小小的单例模式竟然有这么多学问在里面。但是,我们工作中几乎不会去这样写,其实可以用枚举,它是非常好的单例,哈哈哈哈哈,发现说了这么多,竟然工作中不用,但是学习就是这样,量变产生质变。今天的分享到此结束啦!