要保证只有一个单例类的实例被创建,双重检查锁(Double checked locking of Singleton)是一种实现方法。顾名思义,在双重检查锁中,代码会检查两次单例类是否有已存在的实例,一次加锁一次不加锁,一次确保不会有多个实例被创建。顺便提一下,在JDK1.5中,Java修复了其内存模型的问题。在JDK1.5之前,这种方法会有问题。本文中,我们将会看到怎样用Java实现双重检查锁的单例类,为什么Java 5之前的版本双重检查锁会有问题
单例模式的定义:
单例类只能有一个实例。
单例类必须自己创建自己的唯一实例。
单例类必须给所有其他对象提供这一实例。
单例模式的应用非常广泛,例如在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。选择单例模式就是为了避免不一致状态。
单例模式的实现有三种方式:饿汉式(天生线程安全),懒汉式,登记式(可忽略)。
设计线程安全的单例模式
DCL双检查锁机制
其实我觉得能看这篇文章的伙伴们对设计线程安全的单例模式都是有一定的了解,所以对于解决非线程安全的单例模式的3种方式也应该有些了解。我们再来总结一下这三种方式:声明synchronized关键字(同步代码块),DCL双检查锁机制,静态内置类的实现。
当代码的临界区必须在程序执行期内以线程安全的方式获得一次锁时,双检查加锁优化设计模式能减少争用和同步开销。
例子:
class Singleton
{
public:
static Singleton *instance()
{
if(m_instance == NULL)
{
m_instance = new Singleton();
}
return m_instance;
}
void SayHello()
{
std::cout«“Hello”«std::endl;
}
private:
static Singleton m_instance;
};
Singleton Singleton::m_instance = NULL;
上述单例模式在具有抢先多任务或真正的硬件并行性平台上是有问题的。如多个抢先线程在初始化之前同事调用Singleton::instance(); 为了使用临界区不被并发访问,可以使用定界加锁自动得获取和释放一个互斥锁。
class Singleton
{
public:
static Singleton *instance()
{
ScopedLock lock( _mutex() );
if(m_instance == NULL)
{
m_instance = new Singleton();
}
return m_instance;
}
void SayHello()
{
std::cout«“Hello”«std::endl;
}
private:
static Mutex & _mutex()
{
static Mutex m;
return m;
}
static Singleton *m_instance;
};
现在Singleton是线程安全的。不过加锁开销会很大。每次对instance()的调用都有获取和释放锁。可以将哨兵放在条件检查里面,从而避免加锁开销。
static Singleton *instance()
{
if(m_instance == NULL)
{
ScopedLock lock( _mutex() );
m_instance = new Singleton();
}
return m_instance;
}
可以,这种解决方案不能提供线程安全的初始化,因为多线程应用中竞争条件可以使用Singleton多次被初始化。例如:考虑两个同时检查m_instance == NULL的线程,假设两个都成功,一个通过guard获取锁然后释放锁,而另一个将阻塞。在第一个线程初始化Singleton并释放锁后,那个被阻塞的线程会得到锁,并错误的再次初始化Singleton。
解决方案:使用双检查加锁模式
static Singleton *instance()
{
if(m_instance == NULL)
{
ScopedLock lock( _mutex() );
if(m_instance == NULL)
{
m_instance = new Singleton();
}
}
return m_instance;
}
总结
优点:
1.使加锁开销最小。
2.防止竞争条件。
缺点:
1.非原子指针复制语义。如:new之后写内存操作不是原子的,其他线程可能读到一个无效的指针。如将32位指针用于16位总线的计算机上,需要访问两次内存。
2.多处理器缓存的连贯性。一些多处理器平台进行积极的内存缓存优化,可以在多cpu缓存间无序的执行读写操作,必须实现中插入与CPU有关的指令,如清除缓存线的内存障栏。
3.额外的互斥使用。
单例模式的作用就是保证整个应用程序的生命周期中,任何一个时刻,单例类的实例都只存在一个。单例模式主要有三个特点:
1. 单例类只能有一个实例
2. 单例类必须创建自己唯一的实例。
3. 单例类必须为其他对象提供这一实例。
那么如何实现满足上述条件的单例类呢?答案是:
1. 私有构造方法。
2. 内部创建静态对象。
3. 提供静态方法返回该静态对象。
单例设计模式主要有两种实现方法:饿汉式和懒汉式。
1. 饿汉式单例:在定义开始,便实例化自己。
public class Single0 {
private Single0() {
//私有构造方法
}
private static Single0 s=new Single0();//内部创建静态方法,实例化
public static Single0 getsingle() {
return s;//提供静态方法,返回对象
}
}
2. 懒汉式单例:在第一次调用时实例化自己。
class Single1{
private Single1() {
//私有构造方法
}
public static Single1 s=null;
public static Single1 getsingle() {
s=new Single1();//实例化
return s;
}
}
那饿汉式和懒汉式分别有什么区别呢?
1. 线程安全:
饿汉式天生线程安全,可以直接用于多线程而不会出现问题。
懒汉式本身非线程安全,需要人为实现线程安全。
2. 资源加载和性能:
饿汉式在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定的内存,造成内存泄漏,但相应的,在第一次调用时速度也会更快。
而懒汉式顾名思义,会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了。
为了保证懒汉式的线程安全,通常有三种方法:
1. 全局访问点加同步。
由于synchronized是锁方法, 当两个线程都要进入getsingle()时, 只有一个能进入, 并创建出实例, 然后另外一个进入后, 判断 s不为null, 然后直接得到s。 这种做法是没有错误的. 但是由于线程都需要通过getsingle()来获取对象, 所以getsingle()调用频率很高, 所以线程被锁的频率也很高, 所以这种做法效率低。简言之,不管对象有没有被创建,其余线程都只能等待这个方法的锁。
class Single1{
private Single1() {
//私有构造方法
}
public static Single1 s=null;
public static synchronized Single1 getsingle() {
s=new Single1();//实例化
return s;
}
}
2. 双重检查锁定。
由于上述都是在等待同步方法的锁,很容易想到把synchronized放在方法里面,这样可以避免方法的阻塞。
class Single2{
private Single2() {
//私有构造方法
}
public static Single2 s=null;
public static Single2 getsingle() {
if(s==null) {
synchronized(Single2.class) {
s=new Single2();//实例化
}
}
return s;
}
}
这种写法看似没有问题, 其实却有一个很大的隐患, 在于:如果两个线程同时执行getsingle(),判断 instance都不为null后, 进入if判断语句。这个时候一个线程获得锁, 然后进入new了一个对象, 并开心地执行完了。这个时候另外一个线程获得了锁, 但让它也不会再去判断 s是否为null,所以它也会再执行一次new操作。所以这里执行了两次new操作。当然最后s还是只指向后一次new的对象。
所以这个时候需要双重锁定, 就是在 synchronized中再加一次 null判断, 如下:
class Single3{
private Single3() {
//私有构造方法
}
public static Single3 s=null;
public static Single3 getsingle() {
if(s==null) {
synchronized(Single3.class) {
if(s==null) {
s=new Single3();//实例化
}
}
}
return s;
}
}
3. 静态内部类:既实现了线程安全,又避免了同步带来的性能影响。
public class Singleton {
/**
* 内部静态类
*/
private static class SingletonHolder{
private static final Singleton instance=new Singleton();
}
public Singleton() {
System.out.println("产生一个学习委员");
}
public static final Singleton getInstance() {
if(SingletonHolder.instance==null){
return SingletonHolder.instance;
}else {
System.out.println("已经有一个学习委员,不能产生新的学习委员");
return SingletonHolder.instance;
}
}
public void getName(){
System.out.println("我是学习委员:李远远");
} }