1 面试试题
写一个Singleton示例
2 单例设计模式简介
- 单:唯一
- 例:实例
- 单例设计模式,即某个类在整个系统中只能有一个实例对象可被获取和使用的代码模式。
- 例如:代表JVM运行环境的Runtime类
3 单例设计模式要点
-
某个类只能有一个实例
- 构造器私有化
-
它必须自行创建这个实例
- 含有一个该类的静态变量来保存这个唯一的实例
-
它必须自行向整个系统提供这个实例
- 对外提供获取该实例对象的方式:
- 直接暴露
- 用静态变量的get方法获取
- 对外提供获取该实例对象的方式:
4 几种常见形式
-
饿汉式
直接创建对象,不存在线程安全问题(直接暴露对象)
① 构造器私有化
② 自行创建对象,并且用静态变量保存对象
③ 向外提供实例(直接暴露,将静态变量权限为public)
④ 强调这是一个单例,我们可以用final修改
-
直接实例化饿汉式
public class Singleton { private Singleton(){} // 构造器私有化 static final Singleton INSTANCE = new Singleton(); // 静态变量保存对象,直接暴露 public static void main(String[] args) { Singleton s1 = Singleton.INSTANCE; Singleton s2 = Singleton.INSTANCE; System.out.println(s1 == s2); } }
-
枚举式(最简单)
/** * 枚举类型:表示该类型是有限的几个 */ public enum Singleton { INSTANCE; public static void main(String[] args) { Singleton s1 = Singleton.INSTANCE; Singleton s2 = Singleton.INSTANCE; System.out.println(s1 == s2); } }
-
静态代码块饿汉式(适合复杂实例化)
/** * 静态代码块 */ public class Singleton{ // 静态变量保存对象,直接暴露 public static final Singleton INTANCE; private String info; // 构造器中要传入的参数 static { // 在类初始化时,加载调用构造器 INTANCE = new Singleton("default"); // 对象的默认值可以在此处更改,外界获得的都是该值 } // 构造器私有化 private Singleton(String info){ this.info = info; } public static void main(String[] args) { Singleton s1 = Singleton.INTANCE; Singleton s2 = Singleton.INTANCE; System.out.println(s1 == s2); } }
-
-
懒汉式
延迟创建对象
① 构造器私有化
② 一个私有静态变量保存这个唯一的实例对象
③ 对外提供一个静态方法,获取该实例对象
-
线程不安全(适用于单线程)
public class Singleton{ // 私有静态变量保存对象 private static Singleton instance; // 私有化构造器 private Singleton(){} // 对外提供静态方法 public static Singleton getInstance(){ if (instance==null){ instance = new Singleton(); } return instance; } public static void main(String[] args) { Singleton s1 = Singleton.getInstance(); Singleton s2 = Singleton.getInstance(); System.out.println(s1 == s2); } }
-
线程安全(适用于多线程,最常写)
public class Singleton{ // 私有静态变量保存高实例对象 private static Singleton instance; // 私有化构造器 private Singleton(){} // 对外提供静态方法 public static Singleton getInstance(){ if (instance == null){ synchronized (Singleton.class){ if (instance == null){ instance = new Singleton(); } } } return instance; } public static void main(String[] args) { Singleton s1 = Singleton.getInstance(); Singleton s2 = Singleton.getInstance(); System.out.println(s1 == s2); } }
-
静态内部类(适用于多线程,最简单)
/** * 1、内部类被加载和初始化时,才创建INSTANCE实例对象 * 2、静态内部类不会自动创建,随着外部类的加载初始化而初始化,他是要单独去加载和实例化的 * 3、因为是在内部类加载和初始化时,创建的,因此线程安全 */ public class Singleton{ // 构造器私有化 private Singleton(){} // 在内部类里使用私有变量保存该对象 private static class Inner{ private static final Singleton INSTANCE = new Singleton(); } // 对外提供的静态方法 public static Singleton getInstance(){ return Inner.INSTANCE; } public static void main(String[] args) { Singleton s1 = Singleton.getInstance(); Singleton s2 = Singleton.getInstance(); System.out.println(s1 == s2); } }
-
5 多线程模式
5.1 解决方法–加入Synchronized
通过在对外提供静态方法上引入Synchronized关键字,能够解决高并发环境下的单例模式问题。但是synchronized属于重量级的同步机制,它只允许一个线程同时访问获取实例的方法,但是为了保证数据一致性,而减低了并发性。
所以一般采用DCL Double Check Lock 双端检锁机制,就是在进来和出去的时候,进行检测。
public class Singleton {
// 构造器私有化
private Singleton(){
System.out.println(Thread.currentThread().getName()+" 我是构造方法Singleton");
}
// 私有静态变量保存对象
private static Singleton instance;
// 对外提供静态方法
public static Singleton getInstance(){
if (instance == null){
// 同步代码段的时候,进行检测,两端都要添加判断
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}
public static void main(String[] args) {
for (int i = 1; i <= 5; i++) {
new Thread(()->{
Singleton.getInstance();
},String.valueOf(i)).start();
}
}
}
从输出结果来看,确实能够保证单例模式的正确性,但是上面的方法还是存在问题的。
5.2 解决方法–加入volatile
DCL(双端检锁)机制不一定是线程安全的,原因是有指令重排的存在,加入volatile可以禁止指令重排。
因为 instance = new Singleton();可以分为以下三步进行完成:
- memory = allocate(); // 1、分配对象内存空间
- instance(memory); // 2、初始化对象
- instance = memory; // 3、设置instance指向刚刚分配的内存地址,此时instance != null
但是我们通过上面的三个步骤,能够发现,步骤2 和 步骤3之间不存在 数据依赖关系,而且无论重排前 还是重排后,程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。所以会出下面的情况:
- memory = allocate(); // 1、分配对象内存空间
- instance = memory; // 3、设置instance指向刚刚分配的内存地址,此时instance != null,但是对象还没有初始化完成
- instance(memory); // 2、初始化对象
这样就会造成什么问题呢?
也就是当我们执行到重排后的步骤2,试图获取instance的时候,会得到null,因为对象的初始化还没有完成,而是在重排后的步骤3才完成,因此执行单例模式的代码时候,就会重新在创建一个instance实例
指令重排只会保证串行语义的执行一致性(单线程),但并不会关系多线程间的语义一致性
所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,这就造成了线程安全的问题
所以需要引入volatile,来保证出现指令重排的问题,从而保证单例模式的线程安全性
private static volatile Singleton instance = null;
5.3 最终代码
public class Singleton {
// 构造器私有化
private Singleton(){
System.out.println(Thread.currentThread().getName()+" 我是构造方法Singleton");
}
// 私有静态变量保存对象
private static volatile Singleton instance = null;
// 对外提供静态方法
public static Singleton getInstance(){
// a 双重检查加锁多线程情况下会出现某个线程虽然这里已经为空,但是另外一个线程已经执行到d处
if (instance == null){
synchronized (Singleton.class){ //b
//c不加volitale关键字的话有可能会出现尚未完全初始化就获取到的情况。原因是内存模型允许无序写入
if (instance == null){
// d 此时才开始初始化
instance = new Singleton();
}
}
}
return instance;
}
public static void main(String[] args) {
for (int i = 1; i <= 5; i++) {
new Thread(()->{
Singleton.getInstance();
},String.valueOf(i)).start();
}
}
}