四个不同单例模式写法的Java源代码
- 懒汉模式
- 饿汉模式
- 懒汉模式 + 安全线程
- 双重检验锁
第一个懒汉模式线程不安全,后三个都是线程安全的。四者的共有特点,也是单例模式的最主要特点,是其构造函数是私有的。还有一个特点是,有一个静态的getInstance() 方法,静态方法就是类方法。
懒汉模式
这个写法是GoF提出单例模式时候给出的例子,影响力大,写法简单。
package singleton;
// 懒汉模式
public class SingletonLazy
{
private static SingletonLazy uniqueInstance;
private SingletonLazy(){} // 私有的构造函数
public static SingletonLazy getInstance()
{
if(uniqueInstance == null)
{
uniqueInstance = new SingletonLazy();
}
return uniqueInstance;
}
public String toString()
{
return "A simple way to apply Singleton, but is not thread safe";
}
}
然而问题在于:当有多于一个线程的时候,懒汉模式可能失效。举个例子:
- 线程A和线程B,相隔0.1纳秒分别执行getInstance()方法,A先于B
- 时刻”T 纳秒”, A发现 uniqueInstance==null,准备开始new SingletonLazy( )
- 时刻”T+0.1 纳秒”, A还没来得及new完SingletonLazy对象,此时B发现 uniqueInstance==null,也准备开始new SingletonLazy( )
- 时刻”T+0.5 纳秒”, A成功new完了SingletonLazy对象,uniqueInstance!=null
- 时刻”T+0.6 纳秒”, B成功new完了SingletonLazy对象
可以看出A,B两个线程都new了SingletonLazy对象,懒汉模式失效。原因在于:A和B两个线程相隔非常非常短的时间分别执行getInstance(),而且new SingletonLazy对象这个过程需要花费一定的时间。
饿汉模式
饿汉模式写法,是一上来(类加载的时候)就给你实例一个Singleton对象,不管你此时需不需要。回顾一下懒汉模式写法,你一开始不需要Singleton对象,然后程序运行到某一时刻,第一次调用getInstance()方法,才实例一个Singleton对象。饿汉模式的写法,由JVM保证是安全的(虽然内部机制我不懂,我才刚开始学Java),不过简单想一想,Singleton类加载之前,肯定不会有线程new Singleton(),此时Singleton()的构造函数还不存在呢~
可以这么说,饿汉模式解决线程安全问题的方法是:从根子上回避这个问题。想法很好,写法很简单,不过呢要多花费一些空间(牺牲空间,换取时间,这个世界就是很难有两全其美的事情)
package singleton;
// 饿汉模式
public class SingletonEager
{
private static SingletonEager uniqueInstance = new SingletonEager(); // 在这里 new
private SingletonEager(){} // 私有的构造函数
public static SingletonEager getInstance()
{
return uniqueInstance;
}
public String toString()
{
return "Create the unique instance when the class is loaded, which is thread safe";
}
}
懒汉模式(线程安全)
在懒汉模式的基础上,在getInstance() 方法的声明中,增加关键词synchronized,就可以实现线程安全了。毕竟同步嘛,线程A和B即使时间相隔非常非常短,比如相隔0.1纳秒,那也是分先后呀。就因为A快上0.1纳秒,所以就”捷足先登“了,拿到了锁!B在0.1纳秒后,发现getInstance()方法上了锁,进不去了。
package singleton;
public class SingletonThreadSafe
{
private static SingletonThreadSafe uniqueInstance;
private SingletonThreadSafe(){} // 私有的构造函数
// 这里同步了
public static synchronized SingletonThreadSafe getInstance()
{
if(uniqueInstance == null)
{
uniqueInstance = new SingletonThreadSafe();
}
return uniqueInstance;
}
public String toString()
{
return "The getInstance() method is declared with keyword 'synchronized',"
+ " which is thread safe, but with low performance";
}
}
不过《Head First Design Pattern》说:Just keep in mind that synchronizing a method can decrease performance by a factor of 100。相差100倍,这对于程序性能的影响是相当的大呀!
双重检验锁
这个是上面的synchronized方法的升级版本。仔细想一想,只有在第一次getInstance()的时候,才需要new singleton对象,对吧?如果不是第一次getInstance(),那就说明singleton对象已经存在了~于是有了下面的优化代码
package singleton;
// double checked locking
public class SingletonDCL
{ // 注意这个关键词
private volatile static SingletonDCL uniqueInstance;
private SingletonDCL(){} // 私有的构造函数
public static SingletonDCL getInstance()
{
if(uniqueInstance == null) // check once
{
synchronized(SingletonDCL.class)
{
if(uniqueInstance == null) // check twice
{
uniqueInstance = new SingletonDCL();
}
}
}
return uniqueInstance;
}
public String toString()
{
return "A thread safe way to apply Singleton with good performance";
}
}
synchronized的不是一个方法,而是一个方法里面的一个代码块,这样被synchronized的部分减少了。
- 注意1:synchronized前后分别check两次。第一个check,Singleton是否曾经被实例化过;第二个check,就相当于上一个例子”懒汉模式+线程安全“中的check
- 注意2:volatile关键词,这涉及到JVM内部的机制,先强行记住就行了。
测试及补充
package singleton;
public class Main
{
public static void main(String[] args)
{
SingletonLazy singletonLazy = SingletonLazy.getInstance();
SingletonLazy singletonLazy2 = SingletonLazy.getInstance();
System.out.println(singletonLazy);
if(singletonLazy2.equals(singletonLazy))
{
System.out.println("true"); // 同一个引用
}
else
{
System.out.println("false");
}
}
}
运行结果:
A simple way to apply Singleton, but is not thread safe
true
补充:
《Head First Design Pattern》书中单例模式就这4种写法。不过其实还有更多写法,在实验楼网站中,就还有静态内部类写法和枚举类型写法。《Head First Design Pattern》在GitHub的代码中,有一个例子为了让单例模式能够派生出子类,把构造函数和静态数据成员声明为protected(子类访问权限)。
深入单例模式以及其他设计模式,猛戳这里(可以在新标签页中打开~)