你真的懂单例模式吗

在面试中咱们常常会被问到:“你熟悉单例模式吗?请手写一个单例模式的实现?单例模式的应用有哪些……”。有关单例模式的问题比比皆是,在面试中也是很是常见的。html

所谓单例模式就是确保一个类只有一个实例,并对外提供该实例的全局访问点java

实现

类图以下: git

singleton
解读:

  • 实现单例模式的思路:
    • 在类中有一个自身的变量(这个变量能够在使用时建立,也可使用前建立);
    • 确保全局只有该变量的一个实例;
    • 对外提供一个访问该变量的公共方法;
  • 对照上面的实现思路,可知实现步骤分为三步,具备如下特色:
    • 私有的静态变量:加 static 关键字,至关因而一个常量,体现该实例是独一份的特色;
    • 构造函数私有化:目的是不容许其余类执行 new 操做,即不容许其余类建立该类,即也能保证全局只有一个该变量的实例;
    • 提供静态的全局访问点:访问该类的私有变量,因私有变量被 static 关键字修饰,因此获取该变量的公共方法也必须是 static 修饰的;

懒汉式-线程不安全

实现代码

public class LazySingleton {
    // 构造函数私有化
    private LazySingleton(){
        System.out.println("当前线程名称: " + Thread.currentThread().getName() + "\t 我是构造方法...");
    }

    // 私有的静态变量
    private static LazySingleton lazySingleton;

    // 提供静态的全局访问点
    public static LazySingleton getSingleton() {
        if (lazySingleton == null) {
            lazySingleton = new LazySingleton();
        }
        System.out.println(lazySingleton); // 打印当前对象的惟一标识
        return lazySingleton;
    }
}
复制代码

测试代码

public class TestSingletons {
    public static void main(String[] args) {
        // 单线程场景下,直接调用
        LazySingleton.getSingleton();
        LazySingleton.getSingleton();
        LazySingleton.getSingleton();
        LazySingleton.getSingleton();

        // 多线程场景
        /*for (int i = 0; i < 10; i++) { new Thread(() -> { LazySingleton.getSingleton(); }, String.valueOf(i)).start(); }*/
    }
}
复制代码
  1. 打开单线程场景代码并注释多线程场景代码:
  • 运行结果以下:
当前线程名称: main	 我是构造方法...
singleton.LazySingleton@4554617c
singleton.LazySingleton@4554617c
singleton.LazySingleton@4554617c
singleton.LazySingleton@4554617c
复制代码
  • 结果分析: 由结果发现整个过程只构造了一次,这个变量的惟一标识为4554617c,说明单线程场景下是没有问题的
  1. 打开多线程场景代码并注释单线程场景代码:
  • 运行结果以下:
当前线程名称: 2	 我是构造方法...
当前线程名称: 8	 我是构造方法...
当前线程名称: 4	 我是构造方法...
singleton.LazySingleton@ae526cf
当前线程名称: 6	 我是构造方法...
当前线程名称: 0	 我是构造方法...
singleton.LazySingleton@134f6cee
当前线程名称: 3	 我是构造方法...
当前线程名称: 9	 我是构造方法...
singleton.LazySingleton@6e154e44
当前线程名称: 5	 我是构造方法...
当前线程名称: 7	 我是构造方法...
当前线程名称: 1	 我是构造方法...
singleton.LazySingleton@2fd04fd1
singleton.LazySingleton@67c084e5
singleton.LazySingleton@47e3e4b5
singleton.LazySingleton@1b9c704e
singleton.LazySingleton@21279f82
singleton.LazySingleton@2ceb2de
singleton.LazySingleton@14550b42
复制代码
  • 结果分析: 屡次运行结果不一致,实例的惟一标识各不相同,也就是构造了十次,每次都会产生一个新的实例。这说明该实现方式在多线程场景下是没法保证线程安全的

懒汉式-线程安全

在上一个实现方式的基础上加以改进,以求保证在多线程条件下能够达到线程安全的目的。基于这种思路,能够得出懒汉式的另一种实现方式——线程安全的实现方式。github

实现代码

public class LazySafeSingleton {
    // 构造方法私有化
    private LazySafeSingleton(){
        System.out.println("当前线程名称: " + Thread.currentThread().getName() + "\t 我是构造方法...");
    }

    // 私有的静态变量
    private static LazySafeSingleton lazySafeSingleton;

    // 提供同步的静态全局访问点
    public synchronized static LazySafeSingleton getSingleton() {
        if (lazySafeSingleton == null) {
            lazySafeSingleton = new LazySafeSingleton();
        }
        System.out.println(lazySafeSingleton); // 打印当前对象的惟一标识
        return lazySafeSingleton;
    }
}
复制代码

该方式与第一种方式只有一点区别:在提供的全局访问点,即获取实例对象的公共方法加了同步锁,保证同一时刻,只能由一个线程访问 getSingleton() 方法。面试

延伸阅读: java synchronized详解安全

测试代码

public class TestSingletons {
    public static void main(String[] args) {
        // 单线程场景
        LazySafeSingleton.getSingleton();
        LazySafeSingleton.getSingleton();
        LazySafeSingleton.getSingleton();
        LazySafeSingleton.getSingleton();

        // 多线程场景
        /*for (int i = 0; i < 10; i++) { new Thread(() -> { LazySafeSingleton.getSingleton(); }, String.valueOf(i)).start(); }*/
    }
}
复制代码
  1. 单线程场景:
  • 运行结果:
当前线程名称: main	 我是构造方法...
singleton.LazySafeSingleton@4554617c
singleton.LazySafeSingleton@4554617c
singleton.LazySafeSingleton@4554617c
singleton.LazySafeSingleton@4554617c
复制代码
  • 结果分析: 只构造一次,单线程场景下是没有问题的。
  1. 多线程场景下:
  • 运行结果:
当前线程名称: 0	 我是构造方法...
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
复制代码
  • 结果分析: 屡次运行后的结果,会发现都只会构造一次。

双重锁校验-线程安全

分析上一个实现 getSingleton() 方法上加了 synchronized 关键字修饰,虽然能保证同一时刻只能由一个线程访问,保证了多线程场景下的一致性,可是这也会带了另一个问题:并发性下降。因此,接着对懒汉式-线程安全进行改进。bash

实现代码

public class DCLSingleton {
    // 构造方法私有化
    private DCLSingleton(){
        System.out.println("当前线程名称: " + Thread.currentThread().getName() + "\t 我是构造方法...");
    }

    // 静态私有变量
    private volatile static DCLSingleton dclSingleton;

    // 提供静态全局访问点
    public static DCLSingleton getDclSingleton() {
        if (dclSingleton == null) {
            synchronized (DCLSingleton.class) {
                if (dclSingleton == null) {
                    dclSingleton = new DCLSingleton();
                }
            }
        }
        System.out.println(dclSingleton); // 打印当前对象的惟一标识
        return dclSingleton;
    }
}
复制代码

对比上一个实现方式,会发现有两处区别:多线程

  1. 在私有变量上面加了volatile关键字。 缘由是:dclSingleton = new DCLSingleton();编译成字节码后分为三个步骤:
1. 为 dclSingleton 分配内存空间
2. 初始化 dclSingleton
3. 将 dclSingleton 执行分配的内存地址
复制代码

因为jvm具备指令重排的特性,在多线程环境下就可能会出现一个线程获取到的实例还未被初始化的状况。例如:线程T1执行了1和3,此时线程T2调用 getDclSingleton() 方法后发现 dclSingleton 不为空,所以会返回 dclSingleton,可是此时 dclSingleton 还未被初始化。所以在声明静态私有变量时添加volatile关键字保证jvm没法进行指令重排,从而解决上述问题。并发

  1. 原来的同步方法变成了同步块。
if (dclSingleton == null) {
    synchronized (DCLSingleton.class) {
        dclSingleton = new DCLSingleton();
    }
}
复制代码

在只有一个 if 的代码中,多线程条件下,假设线程T1和线程T2同时进入 dclSingleton == null 语句,接着T1或T2其中的一个线程会执行 dclSingleton = new DCLSingleton(); ,在执行结束以后会释放锁,另一个线程也会再次执行 dclSingleton = new DCLSingleton(); 语句,这致使构造函数执行了两次,所以在同步代码块中,须要再次对 dclSingleton 是否为空进行判断。jvm

测试代码

public class TestSingletons {
    public static void main(String[] args) {
        // 单线程
        DCLSingleton.getDclSingleton();
        DCLSingleton.getDclSingleton();
        DCLSingleton.getDclSingleton();
        DCLSingleton.getDclSingleton();

        // 多线程
        /*for (int i = 0; i < 10; i++) { new Thread(() -> { LazySafeSingleton.getSingleton(); }, String.valueOf(i)).start(); }*/
    }

}
复制代码
  1. 单线程场景下:
  • 运行结果:
当前线程名称: main	 我是构造方法...
singleton.DCLSingleton@4554617c
singleton.DCLSingleton@4554617c
singleton.DCLSingleton@4554617c
singleton.DCLSingleton@4554617c
复制代码
  • 结果分析: 构造方法只执行一次,单线程场景下是没有问题的。
  1. 多线程场景下:
  • 运行结果:
当前线程名称: 0	 我是构造方法...
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
复制代码
  • 结果分析: 屡次运行后的结果,会发现都只会构造一次。

饿汉式-线程安全

懒汉式与饿汉式的最主要区别在于,懒汉式的静态私有变量为空,在使用时进行构造;而饿汉式则在加载时就已经构造好了,即在使用前即已经构造完毕。这种方式会形成必定的资源浪费。

public class HungrySingleton {
    // 构造方法私有化
    private HungrySingleton() {
        System.out.println("当前线程名称: " + Thread.currentThread().getName() + "\t 我是构造方法...");
    }

    // 静态私有变量
    private static HungrySingleton hungrySingleton = new HungrySingleton();

    // 提供静态全局访问点
    public static HungrySingleton getSingleton() {
        System.out.println(hungrySingleton); // 打印当前对象的惟一标识
        return hungrySingleton;
    }
}
复制代码

测试方法与上面的懒汉式的建立方式一致,会发现不论是单线程环境下仍是多线程条件下,这种方式都是只会构造一次。

其余方式

静态内部类

对 饿汉式-线程安全 的实现方式进行改进,能够对 建立实例对象 和 使用实例对象 两个步骤进行解耦,即实现使用时在进行建立。静态内部类彻底符合。

public class InnerClazzSingleton {
    // 私有化构造方法
    private InnerClazzSingleton(){}

    // 静态内部类,保证使用时才加载
    private static class InnerClassSingletonHolder {
        private static final InnerClazzSingleton SINGLETON = new InnerClazzSingleton();
    }

    // 提供静态全局访问点
    public static InnerClazzSingleton getInstance() {
        return InnerClassSingletonHolder.SINGLETON;
    }
}
复制代码

这种方式利用了静态内部类在使用时才会进行加载的特性。即调用 getInstance() 方法时 InnerClassSingletonHolder 才会被加载,此时会初始化 SINGLETON 实例,而且也能保证只被初始化一次。这种方式不只具备饿汉式的线程安全的特色,又具备延迟初始化节省系统资源的特色。 测试方法略。

延伸阅读:朝花夕拾——Java静态内部类加载

枚举类

public enum EnumSingleton {
    INSTANCE;

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
复制代码

单例模式的应用

  • Logger Classes
  • Configuration Classes
  • Accesing resources in shared mode
  • Factories implemented as Singletons
  • java.lang.Runtime#getRuntime()
  • java.awt.Desktop#getDesktop()
  • java.lang.System#getSecurityManager()

总结

  1. 手写单例模式的步骤
  • 在类中有一个自身的变量(这个变量能够在使用时建立,也可使用前建立);
  • 确保全局只有该变量的一个实例;
  • 对外提供一个访问该变量的公共方法;
  1. 各类模式的区别
不一样的实现方式 特色
1. 懒汉式-线程不安全 线程不安全;具备延迟加载解决资源的特色;
2. 懒汉式-线程安全 对1进行改造——在全局访问点处添加同步机制。能保证线程安全,虽然说多线程能保证一致性,可是没法保证并发性
3. 双重锁校验式-线程安全 对2进行改造,以求提升并发性,使用volatile 修饰静态实例变量,同步前和同步后均须要校验实例变量是否为空。线程安全
4. 饿汉式-线程安全 对1进行改造,在使用前即会建立实例变量。全局只会建立一次,所以能保证线程安全,可是会形成资源浪费的问题
5. 静态内部类-线程安全 对4进行改造,利用静态内部类使用时才会加载的特性将 实例变量的使用权 和 构造权 解耦。线程安全
6. 枚举类-线程安全 线程安全,多适用于单元素场景

延伸阅读: 单例模式

相关文章
相关标签/搜索