前言
今天就来谈谈常见的设计模式——单例模式。
正文
概念
单例模式属于创建型模式,这个设计模式涉及到一个单一的类,这个类负责创建自己的对象,又确保只有单个对象被创建,提供了一个全局唯一的实例化对象。它的目的是为了控制实例的数目,又或者是实例是单一的情况下,例如一个设备同时只能有一个对象进行操作。
而实现单例模式的关键代码就是将构造函数设为私有,不允许外部创建实例。
设计
饿汉式
实现的方法很简单,我直接给出代码:
1 | public class SingleDemo { |
这里需要注意的是,我们初始化instance是就直接new SingleDemo(),则此时在getInstance就无需判空处理,这种写法被称为饿汉模式。由于装载类时就已经初始化了instance对象,所以这种方法是线程安全的。
懒汉式
懒汉式则是初始将对象设置为null,等有需要时在进行new操作。饿汉式和懒汉式十分形象,饿汉主动找食物,而懒汉等着别人给。
1 | public class SingleDemo { |
懒汉式线程安全
以上的懒汉式并不能保证线程安全,当两个线程同时判断instance == null时,同时通过了判断,导致new出了不同的对象。我们可以通过synchronized关键字锁住getInstance方法保证单例。
1 | public class SingleDemo { |
但这种方法会较为影响效率,由于在静态方法上使用synchronized关键字,所以同时只有一个线程能够进入方法内部,导致效率问题。
双重校验锁(DCL)
先上代码:
1 | public class SingleDemo { |
这个方法就是为了解决上面的效率问题。之所以使用双重检测,是因为多线程情况下,由于方法并不是synchronized修饰的,会导致多个线程同时进入方法进行第一次判空,如果此时instance还未初始化,才会进入临界区域,锁住临界区后,再次进行判空,因为此时前一个线程有可能已经完成了对象创建,而后一个线程才刚刚好进入临界区,所以需要二次判空。
之所以效率比上一个方法高,是因为只有初始化对象时才会进行对象的创建,创建对象的时候才会出现同步的问题,其他情况下直接获取instance就可以了,所以不锁住方法的效率更高一些。
DCL改进
上面的DCL并不是完整的线程安全,原因是JVM在编译时有可能进行指令地重排。举个例子:
例如instance = new SingleDemo(),这一行代码在JVM中可能被分为三个步骤:
- 分配对象的内存
- 初始化对象
- 令instance指向刚才分配的内存
但这个步骤并不是一成不变的,JVM或者CPU会通过优化,改变成以下顺序:
- 分配内存
- 令instance指向刚才分配的内存
- 初始化对象
所以当A线程做完如上1,2步时,instance已经指向了刚才分配的内存了,但并没有初始化,而如果此时B线程才刚开始进行第一次判空时,由于instance已经不是null了,所以B线程直接返回了一个还未初始化的对象。
基于以上原因,我们需要使用volatile修饰instance对象,阻止指令重排,保证instance不会出现中间态。
1 | public class SingleDemo { |
静态内部类
由于从外部无法访问静态内部类,所以只有在getInstance方法才能获得instance对象。
而静态内部类的加载并不是在外部类被加载时被加载,而是当需要使用时才进行加载,来保证懒加载的机制,由于使用的classloader的机制,所以是线程安全的。
1 | public class SingleDemo { |
枚举
以上的方法虽然是私有的构造方法,但是依旧可以使用反射的方法来创建多个实例,关于反射,详见这篇文章。
而有没有什么方法来限制反射呢?答案就是枚举,代码很简单:
1 | public enum SingletonEnum { |
当我们使用反射时获取对象时,会抛出NoSuchMethodException,阻止了反射。
而这种方法可以防止被多次实例化,是线程安全的,并且支持序列化机制,保证反序列化后返回的都是同一个对象,唯一缺点就是并非懒加载。
这种方法实际中使用较少。
总结
今天整理了几种单例模式,对单例模式重新复习了一下,在项目中,最常用的应该就是饿汉模式,其他模式用的较少,需要反序列化则可以使用枚举,或者实现readResolve()方法。
以上,愿好。