单例模式完全解析

本文将探讨单例模式的各种情况,并给出相应的建议。单例模式应该是设计模式中比较简单的一个,但是在多线程并发的环境下使用却是不那么简单了。

首先看最原始的单例模式。

package xylz.study.singleton;

public class Singleton {

    private static Singleton instance = null;

    private Singleton() {
    }

     public static Singleton getInstance() {
         if (instance == null) {
             instance = new Singleton();
         }
         return instance;
     }
 }

显然这个写法在单线程环境下非常好,但是多线程会导致多个实例出现,这个大家都能理解。

最简单的改造方式是添加一个同步锁。

package xylz.study.singleton;

public class SynchronizedSingleton {

    private static SynchronizedSingleton instance = null;

    private SynchronizedSingleton() {
    }

     public static synchronized SynchronizedSingleton getInstance() {
         if (instance == null) {
             instance = new SynchronizedSingleton();
         }
         return instance;
    }
}

显然上面的方法避免了并发的问题,但是由于我们只是在第一次构造对象的时候才需要同步,以后就不再需要同步,所以这里不可避免的有性能开销。于是将锁去掉采用静态的属性来解决同步锁的问题。

package xylz.study.singleton;

public class StaticSingleton {

    private static StaticSingleton instance = new StaticSingleton();

    private StaticSingleton() {
    }

     public static StaticSingleton getInstance() {
         return instance;
    }
}

上面的方法既没有锁又解决了性能问题,看起来已经满足需求了。但是追求“完美”的程序员想延时加载对象,希望在第一次获取的时候才构造对象,于是大家非常聪明的进行改造,也即非常出名的双重检查锁机制出来了。

package xylz.study.singleton;

public class DoubleLockSingleton {

    private static DoubleLockSingleton instance = null;

    private DoubleLockSingleton() {
    }

     public static DoubleLockSingleton getInstance() {
         if (instance == null) {
             synchronized (DoubleLockSingleton.class) {
                 if (instance == null) {
                     instance = new DoubleLockSingleton();
                 }
             }
         }
         return instance;
    }
}

双重锁机制看起来非常巧妙的避免了上面的问题。但是真的是这样的吗?文章《双重检查锁定及单例模式》中谈到了非常多演变的双重锁机制带来的问题,包括比较难以理解的指令重排序机制等。总之就是双重检查锁机制仍然对导致错误问题而不是性能问题。

一种避免上述问题的解决方案是使用volatile关键字,此关键字保证对一个对象修改后能够立即被其它线程看到,也就是避免了指令重排序和可见性问题。参考文章: 指令重排序与happens-before法则。

所以上面的写法就变成了下面的例子。

package xylz.study.singleton;

public class DoubleLockSingleton {

    private static volatile DoubleLockSingleton instance = null;

    private DoubleLockSingleton() {
    }

    public static DoubleLockSingleton getInstance() {
        if (instance == null) {
            synchronized (DoubleLockSingleton.class) {
                if (instance == null) {
                    instance = new DoubleLockSingleton();
                }
            }
        }
        return instance;
    }
}

于是继续改造,某个牛人利用JVM的特性来解决上述问题,具体哪个牛人我忘记了,但是不是下面文章的作者。

  1. Java theory and practice: Fixing the Java Memory Model, Part 2
  2. Initialize-On-Demand Holder Class and Singletons

代码实例:

package xylz.study.singleton;

public class HolderSingleton {

    private static class HolderSingletonHolder {

        static HolderSingleton instance = new HolderSingleton();
    }

    private HolderSingleton() {
        //maybe throw an Exception when doing something
    }

    public static HolderSingleton getInstance() {
        return HolderSingletonHolder.instance;
    }
}

上述代码看起来解决了上面单例模式遇到的所有问题,而且实际上工作的很好,没有什么问题。但是却有一个致命的问题,如果第11行抛出了一个异常,也就是第一次构造函数失败将导致永远无法再次得到构建对象的机会。

使用下面的代码测试下。

package xylz.study.singleton;

public class HolderSingletonTest {

    private static class HolderSingletonHolder {

        static HolderSingletonTest instance = new HolderSingletonTest();
    }

     private static boolean init = false;

     private HolderSingletonTest() {
         //maybe throw an Exception when doing something
         if(!init) {
             init=true;
             throw new RuntimeException("fail");
         }
     }

     public static HolderSingletonTest getInstance() {
         return HolderSingletonHolder.instance;
     }
     public static void main(String[] args) {
         for(int i=0;i<3;i++) {
             try {
                 System.out.println(HolderSingletonTest.getInstance());
             } catch (Exception e) {
                 System.err.println("one->"+i);
                 e.printStackTrace();
             }catch(ExceptionInInitializerError err) {
                 System.err.println("two->"+i);
                 err.printStackTrace();
             }catch(Throwable t) {
                 System.err.println("three->"+i);
                 t.printStackTrace();
             }
         }
     }
}

很不幸将得到以下输出:

two->0
java.lang.ExceptionInInitializerError
    at xylz.study.singleton.HolderSingletonTest.getInstance(HolderSingletonTest.java:21)
    at xylz.study.singleton.HolderSingletonTest.main(HolderSingletonTest.java:26)
Caused by: java.lang.RuntimeException: fail
    at xylz.study.singleton.HolderSingletonTest.<init>(HolderSingletonTest.java:16)
    at xylz.study.singleton.HolderSingletonTest.<init>(HolderSingletonTest.java:12)
    at xylz.study.singleton.HolderSingletonTest$HolderSingletonHolder.<clinit>(HolderSingletonTest.java:7)
     2 more
 three->1
 java.lang.NoClassDefFoundError: Could not initialize class xylz.study.singleton.HolderSingletonTest$HolderSingletonHolder
     at xylz.study.singleton.HolderSingletonTest.getInstance(HolderSingletonTest.java:21)
     at xylz.study.singleton.HolderSingletonTest.main(HolderSingletonTest.java:26)
 three->2
 java.lang.NoClassDefFoundError: Could not initialize class xylz.study.singleton.HolderSingletonTest$HolderSingletonHolder
     at xylz.study.singleton.HolderSingletonTest.getInstance(HolderSingletonTest.java:21)
     at xylz.study.singleton.HolderSingletonTest.main(HolderSingletonTest.java:26)

很显然我们想着第一次加载失败第二次能够加载成功,非常不幸,JVM一旦加载某个类失败将认为此类的定义有问题,将来不再加载,这样就导致我们没有机会再加载。目前看起来没有办法避免此问题。如果要使用JVM的类加载特性就必须保证类加载一定正确,否则此问题将比并发和性能更严重。如果我们的类需要初始话那么就需要想其它办法避免在构造函数中完成。看起来像是又回到了老地方,难道不是么?

总之,结论是目前没有一个十全十美的单例模式,而大多数情况下我们只需要满足我们的需求就行,没必有特意追求最“完美”解决方案。