2009年12月25日星期五

[原创][精品]Java 双重检查锁定与单例模式(无序写入、线程安全)的总结(包含static变量的解释)

单例模式:
  • 模式一:
public class Singleton {
       private static Singleton instance = new Singleton();

       public static Singleton getInstance() {
              return instance;
       }
}
这个模式线程安全,但是也有自己的缺点。当为如下情况时,初始化就会产生问题!

public class SingletonAa {
    private static SingletonAa singletonAa = new SingletonAa();

    public static String name = "sam";

    public static String password;
    static {
        password = "tiger";
    }
    
    private SingletonAa(){
        if(name.equals("sam")) System.out.println("name is ok");
        if(password.equals("tiger")) System.out.println(" password is ok");
    }

    public static SingletonAa getInstance() {
        return singletonAa;
    }
}
因为初始化依赖于static变量,而这些初始化函数在变量被初始化之前被调用了...所以会跑出NullPointException。
  • 模式二:
public class Singleton {
       private static Singleton instance = null;

       public static synchronized Singleton getInstance() {
              if (instance == null) {
                      instance = new Singletion();
              }
              return instance;
       }
}
 
模式一会在程序使用时就初始化instance,而模式二是在第一次调用getInstance时使用。如果初始化较为繁琐,那么显然模式二会更好一点。但是该模式还是有缺点,就是对多线程使用了同步锁。
  • 模式三
public class Singleton {
       private static Singleton instance = null;

       public static synchronized Singleton getInstance() {
              if (instance == null) {
                      synchronized(Singleton.class) {
                              if (instance == null) {
                                        Singleton temp = new Singletion();    //防止无序写入导致的多线程问题
                                        instance = temp;
                              }
                      }
              }
              return instance;
       }
}

以上的模式是线程安全的,又是延迟初始化,而且同步的范围缩到最小。其中创建temp的原因是为了避免Java的无序写入导致的初始化未完成就返回结果。具体参看:http://www.ibm.com/developerworks/cn/java/j-dcl.html

  • 模式四:
是由Bob lee创建的。即线程安全又延迟初始化。非常完美的版本!

public class Singleton {
    static class SingletonsHolder {
        static Singletons instance = new Singletons();
    }
   
    public static Singletons getInstances() {
        return SingletonsHolder.instance;
    }
}
起初我看到这个代码的时候还认为它不是线程安全的,因为考虑到并发情况下,instance可能被初始化多次,所以得到的并不是单例而是多个。
但事实上经过测试得到的是同一单例,测试代码如下:

public class Singletons {
    public static boolean b = true;
    private Singletons() {
        System.out.println("In " + b);
        if (b) {
            System.out.println("Sleeping " + b);
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("Singletons " + b);
    }
   
    static class SingletonsHolder {
        static Singletons instance = new Singletons();
    }
   
    public static Singletons getInstances() {
        System.out.println("getInstances");
        return SingletonsHolder.instance;
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("Start");
        new Thread() {
            public void run() {
                System.out.println(Singletons.getInstances());
            }
        }.start();
        Thread.sleep(1000);
        Singletons.b = false;
        new Thread() {
            public void run() {
                System.out.println("Two Start");
                System.out.println(Singletons.getInstances());
                System.out.println("Two End");
            }
        }.start();
        System.out.println("End ");
    }
}
上述代码完成的过程是:
  • 开始;
  • 线程1获取单例;
  • b为true,故线程1 sleep 5秒;
  • 主线程sleep 1秒;
  • b被设置为false;
  • 线程2获取单例;
得到的输出是:
Start
getInstance
In true
Sleeping true
等待了1秒
End
Two Start
getInstance
等待了4秒
Singletons false
org.my.test.singleons.Singletons@1a758cb
org.my.test.singleons.Singletons@1a758cb
Two End
可见最终得到的单例是同一个。分析我们知道测试的执行过程为:
  • 开始;
  • 线程1获取单例,进入Singletons初始化函数
  • b为true,故线程1 sleep 5秒;
  • 主线程sleep 1秒;
  • b被设置为false;
  • 线程2获取单例,进入Singletons的getInstances()方法,发现SingletonsHolder还未被初始化完成,并且发现它正在被初始化,故等待;
  • 线程1成功初始化instance完成,并输出结果;
  • 线程2等待结束获取了初始化完成的instance,并输出结果;
可见static变量被多线程访问会等待变量初始化的完成,然后才能够访问,从而确保只有一个实例的存在。

没有评论: