对于单例模式面试官会怎样提问呢?你又该如何回答呢?

前言

在面试的时候面试官会怎么在单例模式中提问呢?你又该如何回答呢?可能你在面试的时候你会碰到这些问题:java

  • 为何说饿汉式单例天生就是线程安全的?
  • 传统的懒汉式单例为何是非线程安全的?
  • 怎么修改传统的懒汉式单例,使其线程变得安全?
  • 线程安全的单例的实现还有哪些,怎么实现?
  • 双重检查模式、Volatile关键字 在单例模式中的应用
  • ThreadLocal 在单例模式中的应用
  • 枚举式单例

那咱们该怎么回答呢?那答案来了,看完接下来的内容就能够跟面试官唠唠单例模式了面试


单例模式简介

单例模式是一种经常使用的软件设计模式,其属于建立型模式,其含义便是一个类只有一个实例,并为整个系统提供一个全局访问点 (向整个系统提供这个实)。设计模式

结构:
安全

单例模式三要素:多线程

  • 私有的构造方法;
  • 私有静态实例引用;
  • 返回静态实例的静态公有方法。

单例模式的优势

  • 在内存中只有一个对象,节省内存空间;
  • 避免频繁的建立销毁对象,能够提升性能;
  • 避免对共享资源的多重占用,简化访问;
  • 为整个系统提供一个全局访问点。

单例模式的注意事项

  在使用单例模式时,咱们必须使用单例类提供的公有工厂方法获得单例对象,而不该该使用反射来建立,使用反射将会破坏单例模式 ,将会实例化一个新对象。并发

单线程实现方式

在单线程环境下,单例模式根据实例化对象时机的不一样分为,ide

  • 饿汉式单例(当即加载)饿汉式单例在单例类被加载时候,就实例化一个对象并将引用所指向的这个实例;
  • 懒汉式单例(延迟加载),只有在须要使用的时候才会实例化一个对象将引用所指向的这个实例。

从速度和反应时间角度来说,饿汉式(又称当即加载)要好一些;从资源利用效率上说,懒汉式(又称延迟加载)要好一些。函数


饿汉式单例

// 饿汉式单例
public class HungrySingleton{

    // 私有静态实例引用,建立私有静态实例,并将引用所指向的实例
    private static HungrySingleton singleton = new HungrySingleton();
    // 私有的构造方法
    private HungrySingleton(){}
    //返回静态实例的静态公有方法,静态工厂方法
    public static HungrySingleton getSingleton(){
        return singleton;
    }
}

饿汉式单例,在类被加载时,就会实例化一个对象并将引用所指向的这个实例;更重要的是,因为这个类在整个生命周期中只会被加载一次,只会被建立一次,所以恶汉式单例线程安全的。性能


那饿汉式单例为何是天生就线程安全呢?

由于类加载的方式是按需加载,且只加载一次。因为一个类在整个生命周期中只会被加载一次,在线程访问单例对象以前就已经建立好了,且仅此一个实例。即线程每次都只能也一定只能够拿到这个惟一的对象。测试


懒汉式单例

// 懒汉式单例
public class LazySingleton {
    // 私有静态实例引用
    private static LazySingleton singleton;
    // 私有的构造方法
    private LazySingleton(){}
    // 返回静态实例的静态公有方法,静态工厂方法
    public static LazySingleton getSingleton(){
        //当须要建立类的时候建立单例类,并将引用所指向的实例
        if (singleton == null) {
            singleton = new LazySingleton();
        }
        return singleton;
    }
}

懒汉式单例是延迟加载,只有在须要使用的时候才会实例化一个对象,并将引用所指向的这个对象。

因为是须要时建立,在多线程环境是不安全的,可能会并发建立实例,出现多实例的状况,单例模式的初衷是相背离的。那咱们须要怎么避免呢?能够看接下来的多线程中单例模式的实现形式。


那为何传统的懒汉式单例为何是非线程安全的?

非线程安全主要缘由是,会有多个线程同时进入建立实例(if (singleton == null) {}代码块)的状况发生。当这种这种情形发生后,该单例类就会建立出多个实例,违背单例模式的初衷。所以,传统的懒汉式单例是非线程安全的。


多线程实现方式

  在单线程环境下,不管是饿汉式单例仍是懒汉式单例,它们都可以正常工做。可是,在多线程环境下就有可能发生变异:

  • 饿汉式单例天生就是线程安全的,能够直接用于多线程而不会出现问题
  • 懒汉式单例自己是非线程安全的,所以就会出现多个实例的状况,与单例模式的初衷是相背离的。

那咱们应该怎么在懒汉的基础上改造呢?

  • synchronized方法
  • synchronized块
  • 使用内部类实现延迟加载

synchronized方法

// 线程安全的懒汉式单例
public class SynchronizedSingleton {
    private static SynchronizedSingleton synchronizedSingleton;
    private SynchronizedSingleton(){}
    // 使用 synchronized 修饰,临界资源的同步互斥访问
    public static synchronized SynchronizedSingleton getSingleton(){
        if (synchronizedSingleton == null) {
            synchronizedSingleton = new SynchronizedSingleton();
        }
        return synchronizedSingleton;
    }
}

  使用 synchronized 修饰 getSingleton()方法,将getSingleton()方法进行加锁,实现对临界资源的同步互斥访问,以此来保证单例。

虽然可现实线程安全,但因为同步的做用域偏大、锁的粒度有点粗,会致使运行效率会很低。


synchronized块

// 线程安全的懒汉式单例
public class BlockSingleton {
    private static BlockSingleton singleton;
    private BlockSingleton(){}
    public static BlockSingleton getSingleton2(){
        synchronized(BlockSingleton.class){  // 使用 synchronized 块,临界资源的同步互斥访问
            if (singleton == null) { 
                singleton = new BlockSingleton();
            }
        }
        return singleton;
    }
}

 其实synchronized块跟synchronized方法相似,效率都偏低。


使用内部类实现延迟加载

// 线程安全的懒汉式单例
public class InsideSingleton {
    // 私有内部类,按需加载,用时加载,也就是延迟加载
    private static class Holder {
        private static InsideSingleton insideSingleton = new InsideSingleton();
    }
    private InsideSingleton() {
    }
    public static InsideSingleton getSingleton() {
        return Holder.insideSingleton;
    }
}
  • 如上述代码所示,咱们可使用内部类实现线程安全的懒汉式单例,这种方式也是一种效率比较高的作法。其跟饿汉式单例原理是相同的, 但可能还存在反射攻击或者反序列化攻击 。

双重检查(Double-Check idiom)现实

双重检查(Double-Check idiom)-volatile

使用双重检测同步延迟加载去建立单例,不但保证了单例,并且提升了程序运行效率。

// 线程安全的懒汉式单例
public class DoubleCheckSingleton {
    //使用volatile关键字防止重排序,由于 new Instance()是一个非原子操做,可能建立一个不完整的实例
    private static volatile DoubleCheckSingleton singleton;
    private DoubleCheckSingleton() {
    }

    public static DoubleCheckSingleton getSingleton() {
        // Double-Check idiom
        if (singleton == null) {
            synchronized (DoubleCheckSingleton.class) {       
                // 只需在第一次建立实例时才同步
                if (singleton == null) {      
                    singleton = new DoubleCheckSingleton();      
                }
            }
        }
        return singleton;
    }

}

为了在保证单例的前提下提升运行效率,咱们须要对singleton实例进行第二次检查,为的式避开过多的同步(由于同步只需在第一次建立实例时才同步,一旦建立成功,之后获取实例时就不须要同步获取锁了)。

但须要注意的必须使用volatile关键字修饰单例引用,为何呢?

 若是没有使用volatile关键字是可能会致使指令重排序状况出现,在Singleton 构造函数体执行以前,变量 singleton可能提早成为非 null 的,即赋值语句在对象实例化以前调用,此时别的线程将获得的是一个不完整(未初始化)的对象,会致使系统崩溃。

此可能为程序执行步骤:

  1. 线程 1 进入 getSingleton() 方法,因为 singleton 为 null,线程 1 进入 synchronized 块 ;
  2. 一样因为 singleton为 null,线程 1 直接前进到 singleton = new DoubleCheckSingleton()处,在new对象的时候出现重排序,致使在构造函数执行以前,使实例成为非 null,而且该实例并未初始化的(缘由在NOTE);
  3. 此时,线程 2 检查实例是否为 null。因为实例不为 null,线程 2 获得一个不完整(未初始化)的 Singleton 对象
  4. 线程 1 经过运行 Singleton对象的构造函数来完成对该对象的初始化。

  这种安全隐患正是因为指令重排序的问题所致使的。而volatile 关键字正好能够完美解决了这个问题。使用volatile关键字修饰单例引用就能够避免上述灾难。

NOTE

new 操做会进行三步走,预想中的执行步骤:

memory = allocate();        //1:分配对象的内存空间
ctorInstance(memory);       //2:初始化对象
singleton = memory;        //3:使singleton3指向刚分配的内存地址

但实际上,这个过程可能发生无序写入(指令重排序),可能会致使所下执行步骤

memory = allocate();        //1:分配对象的内存空间
singleton3 = memory;        //3:使singleton3指向刚分配的内存地址
ctorInstance(memory);       //2:初始化对象

双重检查(Double-Check idiom)-ThreadLocal

  借助于 ThreadLocal,咱们能够实现双重检查模式的变体。咱们将临界资源线程局部化,具体到本例就是将双重检测的第一层检测条件 if (instance == null) 转换为 线程局部范围内的操做 。

// 线程安全的懒汉式单例
public class ThreadLocalSingleton 
    // ThreadLocal 线程局部变量
    private static ThreadLocal<ThreadLocalSingleton> threadLocal = new ThreadLocal<ThreadLocalSingleton>();
    private static ThreadLocalSingleton singleton = null;
    private ThreadLocalSingleton(){}
    public static ThreadLocalSingleton getSingleton(){
        if (threadLocal.get() == null) {        // 第一次检查:该线程是否第一次访问
            createSingleton();
        }
        return singleton;
    }

    public static void createSingleton(){
        synchronized (ThreadLocalSingleton.class) {
            if (singleton == null) {          // 第二次检查:该单例是否被建立
                singleton = new ThreadLocalSingleton();   // 只执行一次
            }
        }
        threadLocal.set(singleton);      // 将单例放入当前线程的局部变量中 
    }
}

借助于 ThreadLocal,咱们也能够实现线程安全的懒汉式单例。但与直接双重检查模式使用,使用ThreadLocal的实如今效率上还不如双重检查锁定。


枚举实现方式

它不只能避免多线程同步问题,并且还能防止反序列化从新建立新的对象,

直接经过Singleton.INSTANCE.whateverMethod()的方式调用便可。方便、简洁又安全。

public enum EnumSingleton {
    instance;
    public void whateverMethod(){
        //dosomething
    }
}

测试单例线程安全性

 使用多个线程,并使用hashCode值计算每一个实例的值,值相同为同一实例,不然为不一样实例。

public class Test {
    public static void main(String[] args) {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new TestThread();

        }
        for (int i = 0; i < threads.length; i++) {
            threads[i].start();

        }
    }
}
class TestThread extends Thread {
    @Override
    public void run() {
        // 对于不一样单例模式的实现,只需更改相应的单例类名及其公有静态工厂方法名便可
        int hash = Singleton5.getSingleton5().hashCode();  
        System.out.println(hash);
    }
}

小结

单例模式是 Java 中最简单,也是最基础,最经常使用的设计模式之一。在运行期间,保证某个类只建立一个实例,保证一个类仅有一个实例,并提供一个访问它的全局访问点 ,介绍单例模式的各类写法:

  • 饿汉式单例(线程安全)
  • 懒汉式单例

    • 传统懒汉式单例(线程安全);
    • 使用synchronized方法实(线程安全);
    • 使用synchronized块实现懒汉式单例(线程安全);
    • 使用静态内部类实现懒汉式单例(线程安全)。
  • 使用双重检查模式

    • 使用volatile关键字(线程安全);
    • 使用ThreadLocal实现懒汉式单例(线程安全)。
  • 枚举式单例
各位看官还能够吗?喜欢的话,动动手指点个💗,点个关注呗!!谢谢支持!
欢迎扫码关注,原创技术文章第一时间推出
相关文章
相关标签/搜索