在面试中咱们常常会被问到:“你熟悉单例模式吗?请手写一个单例模式的实现?单例模式的应用有哪些……”。有关单例模式的问题比比皆是,在面试中也是很是常见的。html
所谓单例模式就是确保一个类只有一个实例,并对外提供该实例的全局访问点。java
类图以下: git
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(); }*/
}
}
复制代码
当前线程名称: main 我是构造方法...
singleton.LazySingleton@4554617c
singleton.LazySingleton@4554617c
singleton.LazySingleton@4554617c
singleton.LazySingleton@4554617c
复制代码
4554617c
,说明单线程场景下是没有问题的
。当前线程名称: 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(); }*/
}
}
复制代码
当前线程名称: main 我是构造方法...
singleton.LazySafeSingleton@4554617c
singleton.LazySafeSingleton@4554617c
singleton.LazySafeSingleton@4554617c
singleton.LazySafeSingleton@4554617c
复制代码
当前线程名称: 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;
}
}
复制代码
对比上一个实现方式,会发现有两处区别:多线程
volatile
关键字。 缘由是:dclSingleton = new DCLSingleton();
编译成字节码后分为三个步骤:1. 为 dclSingleton 分配内存空间
2. 初始化 dclSingleton
3. 将 dclSingleton 执行分配的内存地址
复制代码
因为jvm具备指令重排的特性,在多线程环境下就可能会出现一个线程获取到的实例还未被初始化的状况
。例如:线程T1执行了1和3,此时线程T2调用 getDclSingleton() 方法后发现 dclSingleton 不为空,所以会返回 dclSingleton,可是此时 dclSingleton 还未被初始化。所以在声明静态私有变量时添加volatile关键字保证jvm没法进行指令重排,从而解决上述问题。并发
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(); }*/
}
}
复制代码
当前线程名称: main 我是构造方法...
singleton.DCLSingleton@4554617c
singleton.DCLSingleton@4554617c
singleton.DCLSingleton@4554617c
singleton.DCLSingleton@4554617c
复制代码
当前线程名称: 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;
}
}
复制代码
不一样的实现方式 | 特色 |
---|---|
1. 懒汉式-线程不安全 | 线程不安全;具备延迟加载解决资源的特色; |
2. 懒汉式-线程安全 | 对1进行改造——在全局访问点处添加同步机制。能保证线程安全,虽然说多线程能保证一致性,可是没法保证并发性 |
3. 双重锁校验式-线程安全 | 对2进行改造,以求提升并发性,使用volatile 修饰静态实例变量,同步前和同步后均须要校验实例变量是否为空。线程安全 |
4. 饿汉式-线程安全 | 对1进行改造,在使用前即会建立实例变量。全局只会建立一次,所以能保证线程安全,可是会形成资源浪费的问题 |
5. 静态内部类-线程安全 | 对4进行改造,利用静态内部类使用时才会加载的特性将 实例变量的使用权 和 构造权 解耦。线程安全 |
6. 枚举类-线程安全 | 线程安全,多适用于单元素场景 |
延伸阅读: 单例模式