单例模式

原文地址 https://www.jianshu.com/p/9f88630d9667

 

这可能是我们遇上的最简单的模式了,不过他真的简单么?面试的时候如果真的细问你,你可能真的会被问的哑口无言。什么你不信,那我们来一起思考一下下面几个问题
1,单例模式定义
2,写一下你知道的所有单例模式
3,写一个饿汉式单例模式
4,写一个懒汉式单例模式
5,写出你知道的能保证线程安全的单例模式,以及说一说他们为什么能保证线程安全
6,单例如何和其他设计模式结合使用

哈哈,现在的你是不是一脸懵逼,这些问题你都能行云流水的回答上来么,看完本篇你就可以

1,单例模式定义

这个问题不难,大家应该都知道,总结起来一句话

单例模式:一个类只能创建一个对象

类图如下:

 

单例模式

单例模式.png

2,写一下你知道的所有单例模式

单例最简单的写法

public class SingleHungry {
    private SingleHungry(){}
    private static final SingleHungry INSTANCE = new SingleHungry();
    public static SingleHungry getInstance(){
        return INSTANCE;
    }
}

其实吧我们工作上知道这种最简单的写法就可以了,但是我们面试的时候他们一定会问题后几个问题。

3,写一个饿汉式单例模式

我们在2中写的单例就是一个饿汉式的单例模式,有同学可能会问啥叫饿汉式,懒汉式啊。其实他说的就是创建对象的时机,如果你还没使用单例就创建了单例对象那你说你是不是很饥饿,这种情况就叫饿汉式。如果在使用单例的时候才去创建单例,你也是够懒的因此叫懒汉式。

4,写一个懒汉式单例模式

懒汉式不就是延迟加载,在使用的时候才去创建单例的对象这还不简单,刷刷写下下面代码

public class SingleLazy {
    private SingleLazy(){}
    private static SingleLazy INSTANCE = null;
    public static SingleLazy getInstance(){
        if (INSTANCE == null){
            INSTANCE = new SingleLazy();
        }
        return INSTANCE;
    }
}

那这个单例有没有问题呢?

5,写出你知道的能保证线程安全的单例模式,以及说一说他们为什么能保证线程安全

第一种线程安全单例:问题1中的就是一个线程安全的
它为什么能保证线程安全下面有解释


第二种线程安全单例:

4问题中的单例答案是有问题,它的问题在于在多线程状态下是不安全的,那我们如何写能让它变成线程安全的呢,那就是加同步关键字被

public class SingleSafeLazy {
    private SingleSafeLazy(){}

    //这里使用volatile 是保证多线程状态下可见的
    private static volatile SingleSafeLazy INSTANCE = null;

    //同步锁保证多线程安全
    public static synchronized SingleSafeLazy getInstance(){
        if (INSTANCE == null){
            INSTANCE = new SingleSafeLazy();
        }
        return INSTANCE;
    }
}

如何保证线程同步的呢?

1,我们在getInstance()方法添加了synchronized关键字保证了线程同步
2,在给成员变量添加了volatile关键字,这时候一定会有好事的面试官问题volatile关键字是干啥的,这个关键字的作用是保证多线程状态下的可见性和顺序性,注意它不能保证原子性(这里可能也会有面试官问你会不会保证原子性)
3,有的面试官会问那我把synchronized去掉了为什么就会导致线程不安全了呢,这你一定不要怂,我们往下看

 

单例模式

image.png

 

我们来看当如果说我们去掉了synchronized那么当线程2执行到了第13行,还没有创建成功单例对象,现在1执行到第12行,判断INSTNCE也是null,这时候线程1也会进入到13行,这样就会创建两个对象导致数据不一致。

好了我们看了第一个线程安全的单例模式,下面我们来思考一下,这个第一个线程安全的单例模式真的好么,我们每次在getInstance()的时候都会判断同步,这样的效率是不高的,可不可以改改呢,来我们继续


第三种线程安全单例:第二种升级版

public class SingleSafeLazyUpper {
    private SingleSafeLazyUpper(){}

    //这里使用volatile 是保证多线程状态下可见的
    private static volatile SingleSafeLazyUpper INSTANCE = null;

    //同步锁保证多线程安全
    public static SingleSafeLazyUpper getInstance(){
        if (INSTANCE == null){
            synchronized (SingleSafeLazyUpper.class){
                if (INSTANCE == null){
                    INSTANCE = new SingleSafeLazyUpper();
                }
            }
        }
        return INSTANCE;
    }
}

我们把synchronized关键字放到了判断非null之后,这样只有在单例为null的时候才需要加同步,提高了效率,不过你有没有注意到,我们在同步的里面又加了一个判断null的语句这是为什么呢?

 

单例模式

image.png

我们看不加判断null的状态,线程1执行到17行,这时候线程2执行了null判断,它会等在16行,线程1出了同步代码块,线程2会立刻进入同步代码块中这样就会导致创建了两个对象,所以要加判空

那除了这两个这么复杂有没有简单点的多线程安全的呢,答案是有的


第四种线程安全单例:静态内部类方式

public class SingleInnerClass {

    private SingleInnerClass(){}

    private static class SingleHolder{
        static final SingleInnerClass INSTANCE = new SingleInnerClass();
    }
    public static SingleInnerClass getInstance(){
        return SingleHolder.INSTANCE;
    }
}

首先这个也是懒汉式,使用了内部类的方式,在使用的时候在去创建单例对象,并且是线程安全的,那么它为啥就是线程安全的呢,市面上好像很少有人去说这个事,其实这个就是因为happen-before原则,java在创建的对象的时候是线程安全的,什么你说你不知道啥是happen-before原则,那快去看看《java并发编程实战》


第五种线程安全单例:枚举
这是最简单的一种线程安全的单例,下面给你看看有多简单

public enum  SingleEnum {
    INSTANCE
}

一行代码结束一个单例的创建,很厉害是不是,他能保证单例也是happen-before原则


6,单例如何和其他设计模式结合使用

可以使用单例工厂来实现工厂模式,详见工厂模式


总结
我们写的第一个单例能保证线程安全也是happen-before原则
从一个简单的单例可以看出,如果追问细节的话其实我们都会含糊,所以我们平时学习的时候多思考多注意细节在面试的时候就不会被问的哑口无言了,想比别人更优秀就需要比别人多思考