一. 单例模式的概念

  1. 单例模式(Singleton Pattern)确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例。<br /> 三个要点:<br /> 1)这个类只能由一个实例。<br /> 2)它必须自己创建这个实例。<br /> 3)它必须自行向整个系统提供这个实例。<br /> 单例模式通过确保全局只有一个实例来实现某些值的唯一性以及避免系统资源浪费。常见的应用场景包括:第三方token获取,序号生成,计时器等等。

二. 单例模式的实现

  1. 单例模式相对于其他设计模式概念非常简单,但是单例模式的实现有一些讲究,单实例的确保在单线程程序中十分简单,但设计到多线程并发时可能有意想不到的结果。单例模式的实现套路多种多样,涉及不少Java知识点,值得琢磨一翻。

饿汉式

  1. 饿汉式是最简单的单例实现方式,在程序启动时就把类实例化。
  1. public class Singleton{
  2. private static Singleton instance=new Singleton();
  3. private Singleton(){
  4. }
  5. public static Singleton getInstance(){
  6. return instance;
  7. }
  8. }
  1. 可以看到单例模式实现单实例和自己创建实例的方法是通过把**类的构造方法设置成private**并**定义一个static的成员变量来保存自身的实例。**类的成员instance设置成static,它会在项目启动时就被初始化为new Singleton(),且在全局中只会实例化一次。static的成员变量属于类本身而不是对象,所以在类外部无需实例化Singleton的对象即可以通过Singleton.getInstance()来访问instance。<br /> 饿汉式实现的主要问题在于类的实例化太早,项目中还未使用到这个类,类就已经被实例化并占据相应系统资源。但是我个人认为如果项目中的单例并不多(一般也是如此),而且考虑到单例既然被设计出来在项目中就迟早会被访问到,这个实现方式导致的资源浪费可能往往微乎其微,所以饿汉式单例实现也未尝不是种简单有效的方法。

懒汉式

  1. 既然不想在项目启动时就实例化,那我们自然而然的就想到在instance被访问时再去实例化。
  1. public class Singleton{
  2. private static Singleton instance=null;
  3. private Singleton(){
  4. }
  5. public static Singleton getInstance(){
  6. if (instance==null){
  7. instance=new Singleton();
  8. }
  9. return instance;
  10. }
  11. }


这个实现看起来很完美,简单的一个 if 就解决了饿汉式的弊病。遗憾的是,这种实现方式在多线程并发的情况下是无法保证单例的。我们可以很容易的想象如下情况,instance尚未被实例化,此时线程A通过第8行进入if代码块,然而在第九行执行前,线程B也通过第8行进入了if代码块,这种情况下instance就一定会被实例化两次。
为了解决这个问题,一个很直观的想法就是利用 java的synctroniazed关键字:

  1. public class Singleton{
  2. private static Singleton instance=null;
  3. private Singleton(){
  4. }
  5. public static Singleton getInstance(){
  6. if (instance==null){
  7. synchronized(Singleton.Class){
  8. if (instance==null){
  9. instance=new Singleton();
  10. }
  11. }
  12. }
  13. return instance;
  14. }
  15. }
  1. 注意到在synchronized代码块中还需要进行一次if判断,否则这个同步代码块就没有作用,这种用两次if来进行判断的方式被称为**双重检查锁定(Double-Check Locking)**。<br /> 问题到这里就结束了吗?没有,实际上以上的代码还是无法确保单例。在java中每个线程是有自己的内存区域的,相对于主内存这个内存区域有类似缓存的作用。回顾之前的例子,线程A进入同步代码块并创建实例后,首先更新自己内存区域中instance的值,随后才会更新主内存中instance的值。如果在这两次更新之间,线程B进入了同步代码块,就会在主内存instance值未更新的情况下运行第二个if判断并成功进入代码块。这样一来,instance又被实例化了两次。<br /> 实际上要彻底解决同步的问题,还需要给instance增加一个关键字**volatile**。被volatile关键字修饰的变量被**修改时会立即反映到主内存**,不会出现上文提到的情况。最终的代码如下:<br />
  1. public class Singleton{
  2. private volatile static Singleton instance=null;
  3. private Singleton(){
  4. }
  5. public static Singleton getInstance(){
  6. if (instance==null){
  7. synchronized(Singleton.Class){
  8. if (instance==null){
  9. instance=new Singleton();
  10. }
  11. }
  12. }
  13. return instance;
  14. }
  15. }
  1. 懒汉式实现虽然解决了饿汉式实现的弊端,但是这种实现在实际中基本不会被用到。syntronized关键字的加解锁会导致每次调用instance都要耗费额外的资源和时间;另一方面,volatile关键字也会导致instance被使用处java代码顺序优化被关闭,降低运行效率。

静态内部类实现的懒汉式

  1. 说了这么多,是否有两全其美的单例模式实现呢?这个问题的答案因开发语言而异,幸运的是对于java,两全其美的实现方式是存在的,那就是利用静态内部类。<br />
  1. public class Singleton{
  2. private Singleton(){
  3. }
  4. private static class SingletonHolder(){
  5. private final static Singleton instance=new Instance();
  6. }
  7. public static Singleton getInstance(){
  8. return SingletonHolder.instance;
  9. }
  10. }
  1. 在上面代码中单例模式的性质是如何实现的呢?首先我们需要了解java的类延迟加载机制,在程序启动时,java的类会被编译为.class文件,但是只有当类(或其子类)被第一次实例化或 static成员被访问时,类中才会进行初始化。回到Singleton类,只有在 getInstance被调用时,内部类SingletonHolder才会被初始化并执行new Instance(),如此就解决了饿汉式的资源浪费问题。同时有于java的机制确保了静态内部类是单例的,所以也不用考虑线程安全的问题,解决了懒汉式频繁加解锁的困扰。
  2. 掌握了以上的单例模式实现方法已经足够,不过 Java中引入enum枚举类型后,还可以通过枚举来实现单例模式(有于枚举实际上就是一个非显示定义的类,这种方式本质上还是静态内部类),代码如下:<br />
  1. public class Singleton{
  2. private Singleton(){
  3. }
  4. private enum SingletonHolder(){
  5. INSTANCE;
  6. private Singlton instance;
  7. SingletonHolder(){
  8. this.instance=new Instance();
  9. }
  10. private Singleton getSingleton(){
  11. return this.instance;
  12. }
  13. }
  14. public static Singleton getInstance(){
  15. return SingletonHolder.INSTANCE.getSingleton();
  16. }
  17. }
  1. 内部 enum和内部静态类的性质基本类似,同时这种实现方式充分利用了enum可以定义内部变量和方法的特点,在enum唯一的枚举量INSTANCE被构造时成员变量instance会被实例化。这种实现略微复杂,而且能熟练使用enum也不容易,所以不如直接静态内部类的方式常见。