多线程下的singleton
YI GJ ---2010年5月5日
单例模式是最简单的设计模式之一,但是对于java开发者来说,它却有很多的缺陷。下面讨论单例模式在面对多线程时,如果处理缺陷。
单例模式的用意在于前一段中所关心的。通过单例模式你可以:
确保一个类只有一个实例被建立 ,
提供了一个对对象的全局访问指针 ,
下面看个单例经典例子:
public class Singleton{
private static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance(){
if(instance==null){
instance = new Singleton();//懒汉式单例
}
return instance;
}
}
在例1中的单例模式的实现很容易理解。Singleton类保持了一个对单独的单例实例的静态引用,并且从静态方法getInstance()中返回那个引用。
关于Singleton类,有几个让我们感兴趣的地方。首先,Singleton使用了一个众所周知的懒汉式实例化去创建那个单例类的引用;结果,这个单例类的实例直到getInstance()方法被第一次调用时才被创建。这种技巧可以确保单例类的实例只有在需要时才被建立出来。其次,注意Singleton实现了一个private的构造方法,这样客户端不能直接实例化一个Singleton类的实例。
在单线程情况下,对该类添加main方法进行测试:
public static void main(String[] agrs) {
for (int i = 0; i < 5; i++) {
System.out.println("Singleton.getInstance().hashCode() --"+ i +" -- "+Singleton.getInstance().hashCode());
}
}
测试结果如下:
可以看出在单线程情况下,调用多次 getInstance方法,得到的Singleton的实例的hash码都是一样的。说明只生成了一个实例对象。
但是我们仔细的分析代码 ,会发现其实前面的代码不是线程安全的,如果两个线程,我们称它们为线程1和线程2,在同一时间调用Singleton.getInstance()方法,如果线程1先进入if块,然后线程2进行控制,那么就会有Singleton的两个的实例被创建。尽管这个问题就在这段代码上:
if(instance==null){
instance = new Singleton();//懒汉式单例
}
如果一个线程在第二行的赋值语句发生之前切换,那么成员变量instance仍然是null,然后另一个线程可能接下来进入到if块中。在这种情况下,两个不同的单例类实例就被创建。不幸的是这种假定很少发生,这样这种假定也很难在测试期间出现。下面我们来模拟这个问题的产生。
public class Singleton {
private static Singleton instance = null;
private static boolean flag = true;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
threadSleep();
instance = new Singleton();// 懒汉式单例
}
System.out.println("instance.hashCode() : "+instance.hashCode());
return instance;
}
public static void main(String[] agrs) {
new Thread(new SingletonTestRunnable()).start();
new Thread(new SingletonTestRunnable()).start();
}
private static void threadSleep(){
if(flag){
try {
System.out.println("当前线程休眠5秒!");
Thread.currentThread().sleep(5000);
System.out.println("休眠5秒结束!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class SingletonTestRunnable implements Runnable {
public void run() {
Singleton s = Singleton.getInstance();
}
}
在Singleton中加入一个方法threadSleep(),让当前线程休眠5秒钟,SingletonTestRunnable实现Runnable接口,在run方法中 调用Singleton.getInstance()得到Singleton的实例,运行main方法,测试结果如下:
可以看出对于两个线程得到的Singleton的实例的hashCode是不同的,则可以知道—这是两个不同的实例。分析代码:在第一个线程运行到
if (instance == null) {
threadSleep();
instance = new Singleton();// 懒汉式单例
}
时候休眠5秒钟,随后第二线程进入该代码片段,运行到threadSleep方法时候,也休眠5秒钟,第一个线程醒来,继续向下执行代码 instance = new Singleton(),创建第一个Singleton对象实例,随后第二个线程也执行了 instance = new Singleton(),这样创建了第二个实例对象。
那么我们可以看出,Songleton 不是线程安全的。
尝试修正:
public static Singleton getInstence() {
if (instence == null) {
methodSleep();
synchronized (Singleton .class) {
if (instence == null) {
instence = new Singleton (); //双重检查确保在并发的条件下也是只有一个实例存在。但是由于使用了
return instence; //synchronized关键字,在第一次访问时效率比较慢。。
}
}
}
return instence;
}
第一种方案:在getInstance()方法前面加上 关键字 synchronized ,测试结果如下:
分析这个结果:开启两个线程,只打印了一次测试汉字,这就说明 只调用了一次 threadSleep()方法,而打印出来的hashCode也是相同的,表示引用了同一个Singleton对象实例。
第二种方案:一个简单、快速而又是线程安全的单例模式实现
public class Singleton {
public static Singleton INSTANCE = new Singleton();
private Singleton() {
}
public static void main(String[] agrs) {
new Thread(new SingletonTestRunnable()).start();
new Thread(new SingletonTestRunnable()).start();
}
}
class SingletonTestRunnable implements Runnable {
public void run() {
System.out.println( Singleton.INSTANCE.hashCode());
}
}
测试结果看出两个线程创建的Singleton对象引用指向是同一块堆内存,hashCode相同。
这段代码是线程安全的是因为静态成员变量一定会在类被第一次访问时被创建。你得到了一个自动使用了懒汉式实例化的线程安全的实现。