设计模式之单例模式
聊聊单例模式,面试加分题
犹记得之前面xx时,面试官一上来就问你知道哪些设计模式,来手写一个单例模式的场景;尴尬的我,只写了懒汉式饿汉式,对于单例其他的变种一概不知;这次就来弥补下这方面的知识盲区!
饿汉式
饿汉式,从字面上理解就是很饿,一上来就要吃的,那么它会把吃的先准备好,以满足它的需求;那么对应到程序上的表现就为:在类加载的时候就会首先进行实例的初始化,后面如果应用程序需要这个实例的话,就有现成的了,可以直接使用当前的单例对象!
我们来手写下饿汉式的代码:
public class Singleton{
// 声明静态私有实例 并实例化
private static Singleton singleton = new Singleton();
// 提供对外初始化方法 静态类加载就初始化
public static Singleton initInstance(){
return singleton;
}
// 声明私有构造方法 即在外部类无法通过new 初始化实例
private Singleton(){
}
public void doSomeThing(){
System.out.println("do some thing!");
}
}
class SingletonDemo{
public static void main(String[] args) {
Singleton singleton = Singleton.initInstance();
}
}
饿汉式的优点:它是线程安全的,因为单例对象在类加载的时候就被初始化了,当调用单例对象时只需要去把对应的对象赋值给变量即可!
饿汉式的缺点:如果这个类不经常使用,会造成一定的资源浪费!
懒汉式
懒汉式,就是比较懒,每次需要填饱肚子时才会外出觅食;那么对应到程序层面的理解:当应用程序需要某个对象时,该对象的类就会去创建一个实例,而不是提前准备好的!
我们来手写下懒汉式的代码:
public class Singleton2 {
// 声明私有静态对象
private static Singleton2 singleton2;
// 对外提供初始化方法
public static Singleton2 initInstance(){
if(singleton2 == null){
singleton2 = new Singleton2();
}
return singleton2;
}
// 私有构造器
private Singleton2(){
}
public void doSomeThing(){
System.out.println("do some thing!");
}
}
class SingletonDemo2{
public static void main(String[] args) {
Singleton2 singleton2 = Singleton2.initInstance();
singleton2.doSomeThing();
}
}
同样我们看下懒汉式的优点:不会造成资源的浪费
懒汉式的缺点:多线程情况下,会有线程安全的问题;
上面我们可以看到,饿汉式和懒汉式的唯一区别就是:饿汉式在类加载时就完成了对象的初始化,而懒汉式是在需要初始化的时候再去初始化对象;其实在单线程情况下,他们都是线程安全的;但是我们写的代码,必须考虑多线程情况下的并发问题,那么懒汉式的这种写法基本不满足需求,我们需要做些改造,使得它变得线程安全,满足我们的需求!
双重检测锁
我们知道,懒汉式下对象的初始化在并发环境下,可能多个线程同时执行到singleton2 == null
,从而初始化了多个实例,这就引发了线程安全问题!
我们就需要改写它的初始化方法,我们知道加锁可以解决一般的线程安全问题,synchronized
这个关键字可以修饰一个代码块或方法,被其修饰的方法或代码块就被加了锁;而从某些方面理解,synchronized
是个同步锁,亦是个可重入锁!哈哈,关于锁的种类及概念有点多,后面准备写一篇关于锁的博客来总结下;不再发散了,回归正题
我们来改造下懒汉式的初始化方法如下:
// 对外提供初始化方法
public synchronized static Singleton2 initInstance(){
if(singleton2 == null){
singleton2 = new Singleton2();
}
return singleton2;
}
我们看下上面的代码,初看没什么问题是解决了线程安全问题;但是由于整个方法都被synchronized
修饰,那么在多线程的情况下就增加了线程同步的开销,降低了程序的执行效率;为了改进这个问题,我们将synchronized
放入到方法内,实现代码块的同步;改下如下:
// 对外提供初始化方法
public static Singleton2 initInstance(){
if(singleton2 == null){
synchronized(Singleton2.class){
singleton2 = new Singleton2();
}
}
return singleton2;
}
呃,这样就满足了我们的要求了吗?聪明如你一定发现了,虽然我们将synchronized
移到了方法内部,降低了同步的开销,但是在并发的情况下假设多个线程同时执行到if(singleton2 == null)
时,依旧会排队初始化Singleton2
实例,这样又会造成新的线程安全问题;那么为了解决这个问题,就出现了大名鼎鼎的“双重检测锁”。我们来看下它的实现,将上述代码改写如下:
// 对外提供初始化方法
public static Singleton2 initInstance(){
if(singleton2 == null){// 第一次非空判断
synchronized(Singleton2.class){
if(singleton2 == null)// 第二次非空判断
singleton2 = new Singleton2();
}
}
return singleton2;
}
哈哈,这个双重即是判断两次的意思,并不是加两把锁哈;那么这样就能行了吗?初看没问题啊,但是我们细想之下这样写真的没问题吗?你写的代码,执行的时候真的会按你想的过程执行吗?有没有考虑过指令重排呢?问题就出现在new Singleton2()
这个代码上,这行代码不是一个原子操作!
我们再来回顾下指令重排的大致执行流程:
1.给对象实例分配内存空间
2.调用对象构造方法,初始化成员变量
3.将构造的对象指向分配的内存空间
问题就出在指令重排后,cpu对指令重排的优化上,也就是说上述的三个过程并不是每次都是1-2-3顺序执行的,而是也有可能1-3-2;那么我们试想下并发情况下可能出现的场景,当线程A执行到步骤3时,cpu时间片正好轮询到线程B,那么线程B判断实例已经指向了对应的内存空间,不为null就不会 初始化实例了,就得到了一个未初始化完成的对象,这就导致了问题的诞生!
为了解决这个问题,我们知道还有一个关键字volatile
可以完美的解决指令重排,使得非原子性的操作对其他对象是可见的!(volatile关键字保障了变量的内存的可见性和一致性问题,关于内存屏障可以看我之前的一篇文章JMM 内存模型知识点探究了解)。那么我们将懒汉式改写如下:
public class Singleton2 {
// 声明私有静态对象
private volatile static Singleton2 singleton2;
// 对外提供初始化方法
public static Singleton2 initInstance(){
if(singleton2 == null){
synchronized(Singleton2.class){
if(singleton2 == null)
singleton2 = new Singleton2();
}
}
return singleton2;
}
// 私有构造器
private Singleton2(){
}
public void doSomeThing(){
System.out.println("do some thing!");
}
}
class SingletonDemo2{
public static void main(String[] args) {
Singleton2 singleton2 = Singleton2.initInstance();
singleton2.doSomeThing();
}
}
其实除了上面的单例实现外,还有两种常见的单例实现
静态内部类
代码如下:
public class InnerClassSingleton {
// 私有静态内部类
private static class InnerInstance{
private static final InnerClassSingleton singleton = new InnerClassSingleton();
}
// 对外提供的初始化方法
public static InnerClassSingleton initInstance(){
return InnerInstance.singleton;
}
// 私有构造器
private InnerClassSingleton(){
}
public void doSomeThing(){
System.out.println("do some thing!");
}
}
class InnerClassSingletonDemo{
public static void main(String[] args) {
InnerClassSingleton innerClassSingleton = InnerClassSingleton.initInstance();
innerClassSingleton.doSomeThing();
}
}
其实,静态内部类的方式和饿汉式本质是一样的,都是根据类加载机制来初始化实例,从而保证单例和线程安全的;不同的是静态内部类的方式是按需构建实例,不会如饿汉式一样造成资源浪费的问题;所以这个是饿汉式一个比较好的变种!
枚举类
枚举是比较推荐的一种单例模式,它是线程安全的,且通过反射、序列化以及反序列化都无法破坏它的单例属性(其他的单例采用私有构造器的实现其实并不安全),至于为什么呢?这个可以参考博客:[为什么要用枚举实现单例模式(避免反射、序列化问题)]
代码如下:
public class EnumSingleton {
// 声明私有的枚举类型
private enum Enum{
INSTANCE;
// 声明单例对象
private final EnumSingleton instance;
// 实例化
Enum(){
instance = new EnumSingleton();
}
private EnumSingleton getInstance(){
return instance;
}
}
// 对外提供的初始化方法
public static EnumSingleton initInstance(){
return Enum.INSTANCE.getInstance();
}
// 私有构造器
private EnumSingleton(){
}
public void doSomeThing(){
System.out.println("do some thing!");
}
}
class EnumSingletonDemo{
public static void main(String[] args) {
EnumSingleton enumSingleton = EnumSingleton.initInstance();
enumSingleton.doSomeThing();
}
}
好,至此我们总结了单例的几种实现方式;比较推荐的是后面两种方式,一般懒汉式我们就采用双重检测锁的方式;你可以发散思考下单例的应用场景,例如Spring中的Bean的初始化就是单例模式的典型应用,或者在消息中心中使用比较频繁的短链接!