两种单例模式详解(内含懒汉式的双重校验锁详解)-创新互联
对于一个软件系统的某些类而言,我们无须创建多个实例。举个大家都熟知的例子——Windows任务管理器,我们可以做一个这样的尝试,在Windows的“任务栏”的右键弹出菜单上多次点击“启动任务管理器”,看能否打开多个任务管理器窗口?通常情况下,无论我们启动任务管理多少次,Windows系统始终只能弹出一个任务管理器窗口,也就是说在一个Windows系统中,任务管理器存在唯一性。
实际开发中,我们也经常遇到类似的情况,为了节约系统资源,有时需要确保系统中某个类只有唯一一个实例,当这个唯一实例创建成功之后,我们无法再创建一个同类型的其他对象,所有的操作都只能基于这个唯一实例。为了确保对象的唯一性,我们可以通过单例模式来实现,这就是单例模式的动机所在。
单例模式定义如下: 单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种对象创建型模式。
单例模式有三个要点:
- 某个类只能有一个实例;
- 它必须自行创建这个实例;
- 它必须自行向整个系统提供这个实例。
在单例类的内部实现只生成一个实例,同时它提供一个静态的getInstance()工厂方法,让客户可以访问它的唯一实例;为了防止在外部对其实例化,将其构造函数设计为私有;在单例类内部定义了一个Singleton类型的静态对象,作为外部共享的唯一实例。
单例模式一般分为两种,分别是饿汉式与懒汉式,下面就分别讲解这两种模式,并且详解饿汉式的双重校验锁
饿汉式对于饿汉式来说,它在定义静态变量的时候实例化单例类,因此在类加载的时候就已经创建了单例对象。而且该单例对象用final修饰,之后每次获取直接返回即可。
public class HungrySingleton {private static final HungrySingleton singleton=new HungrySingleton();
private HungrySingleton(){}
public static HungrySingleton getInstance(){return singleton;
}
}
懒汉式(双重校验锁)懒汉式单例在第一次调用getInstance()方法时实例化,在类加载时并不自行实例化,这种技术又称为延迟加载(Lazy Load)技术,即需要的时候再加载实例。
先看一下在单线程情况下的懒汉式单例:
public class LazySingleton {private static LazySingleton singleton=null;
private LazySingleton(){}
public static LazySingleton getInstance(){if(singleton==null){singleton=new LazySingleton();
}
return singleton;
}
}
可以看到,在获取实例的时候,会先判断一下当前的静态变量singleton是否为null,如果为null,则为其初始化。如果不为null,则直接返回。
但是这样的程序在多线程环境下会出现问题!!!
在多线程环境下这样的代码会出现问题,因此我们考虑给getInstance()
方法加上同步锁,防止多个线程同时访问getInstance()
方法,如下。
public class LazySingleton {private static LazySingleton singleton=null;
private LazySingleton(){}
public static synchronized LazySingleton getInstance(){if(singleton==null){singleton=new LazySingleton();
}
return singleton;
}
}
但是这样一来,每次调用getInstance()时都需要进行线程锁定判断,在多线程高并发访问环境中,将会导致系统性能大大降低。那么如何解决该问题呢?有人提出了双重校验锁:
在加锁之前先判断一下该静态变量是否为null。如果不为null就不需要再加锁进行初始化了。这样在高并发的环境下就不会出现频繁获取锁的情况了。
public class LazySingletonSyn {private static LazySingletonSyn singleton=null;
private LazySingletonSyn(){}
public static LazySingletonSyn getInstance(){if(singleton==null){synchronized (LazySingletonSyn.class){if(singleton==null){ // 标记点 1
singleton=new LazySingletonSyn();
}
}
}
return singleton;
}
}
现在程序是完美的了吗?
依然不是!问题出现在singleton=new LazySingletonSyn();
语句
由于现代的处理器大多采用指令级并行技术。为了提高指令的执行效率,在指令执行的阶段可能会出现重排序的现象。
我们看上面代码的标记点1singleton=new LazySingletonSyn();
该语句在执行的时候会被分解为三条(伪)指令:
memory=allocate(); //1.分配对象的内存空间
ctorInstance(memory); //2. 初始化对象
instance=memory; //3. 设置instance指向刚分配的内存地址
在上面的伪代码中,其中2与3可能会被重排序。
这里涉及到了as-if-serial概念。as-if-serial概念是指不管怎么重排序,单线程程序的执行结果不能被改变。而在该语句中,将2,3的执行顺序改变之后,在单线程的情况下该程序的运行结果不会被改变。因此2,3可能重排序
一旦被重排序,就可能出现以下的结果:
如果线程A,B按照下图的时间执行,那么B线程将会得到一个还没有被初始化的对象!!
问题就出现在2,3的重排序!那么我们只需要禁止2,3重排序即可。
双重校验锁实现懒汉式
我们只需要对上面的代码进行很小的改动(将singleton声明为volatile),就可以实现线程安全的懒汉式单例。
public class LazySingletonSyn {private volatile static LazySingletonSyn singleton=null;
private LazySingletonSyn(){}
public static LazySingletonSyn getInstance(){if(singleton==null){synchronized (LazySingletonSyn.class){if(singleton==null){singleton=new LazySingletonSyn();
}
}
}
return singleton;
}
}
解释如下:
由于singleton被volatile修饰,那么为了实现volatile的内存语义(保证singleton在多线程环境下对共享内存的可见性),编译器在生成字节码的时候,会在指令序列中插入内存屏障来禁止特定的指令重排序。
由于singleton=new LazySingletonSyn();
是一个写操作,在该操作的指令之后,JMM(Java内存模型)会插入一个storeload内存屏障。
此时的内存指令变成:
memory=allocate(); //1.分配对象的内存空间
ctorInstance(memory); //2. 初始化对象
instance=memory; //3. 设置instance指向刚分配的内存地址
storeload; //4. storeload内存屏障
该指令(storeload)会保证在屏障之前的所有内存访问指令全部完成之后,再执行该屏障的之后的语句。因此当线程B再尝试singleton==null
的时候,线程A的singleton=new LazySingletonSyn();
以及全部执行完成了。因此就不会再出现上面的由于指令2,3重排序导致的问题了。
其实就是一句话:
JMM会禁止volatile写与其之后可能存在的volatile读/写重排序。因此不会存在上面的图出现的情况!
你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧
文章名称:两种单例模式详解(内含懒汉式的双重校验锁详解)-创新互联
浏览地址:http://scyanting.com/article/docgce.html