深入理解设计模式中的单例模式
4、双检锁/双重校验锁(DCL,即 double-checked locking)
一、什么是单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
我们将创建一个 SingleObject 类。SingleObject 类有它的私有构造函数和本身的一个静态实例。SingleObject 类提供了一个静态方法,供外界获取它的静态实例。SingletonPatternDemo 类使用 SingleObject 类来获取 SingleObject 对象。
二、应用场景
举一个例子,网站的计数器,一般也是采用单例模式实现,如果你存在多个计数器,每一个用户的访问都刷新计数器的值,这样的话你的实计数的值是难以同步的。但是如果采用单例模式实现就不会存在这样的问题,而且还可以避免线程安全问题。同样多线程的线程池的设计一般也是采用单例模式,这是由于线程池需要方便对池中的线程进行控制 同样,对于一些应用程序的日志应用,或者web开发中读取配置文件都适合使用单例模式,如HttpApplication 就是单例的典型应用。 从上述的例子中我们可以总结出适合使用单例模式的场景和优缺点: 适用场景: 1.需要生成唯一序列的环境 2.需要频繁实例化然后销毁的对象。 3.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。 4.方便资源相互通信的环境
三、优缺点
优点:
1.在内存中只有一个对象,节省内存空间;
2.避免频繁的创建销毁对象,可以提高性能;
3.避免对共享资源的多重占用,简化访问;
4.为整个系统提供一个全局访问点。
缺点:
1.不适用于变化频繁的对象;
2.滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共 享连接池对象的程序过多而出现连接池溢出;
4.如果实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,这可能会导致对象状态的丢失;
四、单例模式的几种实现方式
1、懒汉式,线程不安全
是否 Lazy 初始化:是
是否多线程安全:否
实现难度:易
描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。
public class testSingleton1 {
public static void main(String[] args) {
Singleton t1 = Singleton.getInstance();
Singleton t2 = Singleton.getInstance();
System.out.println(t1==t2);
}
}
class Singleton{
private Singleton(){};
private static Singleton singleton=null;
public static Singleton getInstance(){
if(singleton==null){
singleton=new Singleton();
}
return singleton;
}
}
运行结果:
2、懒汉式,线程安全
是否 Lazy 初始化:是
是否多线程安全:是
实现难度:易
描述:这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
优点:第一次调用才初始化,避免内存浪费。
缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。
public class testSingleton2 {
public static void main(String[] args) {
Singleton2 t1 = Singleton2.getInstance();
Singleton2 t2 = Singleton2.getInstance();
System.out.println(t1==t2);
}
}
class Singleton2{
private Singleton2(){};
private static Singleton2 singleton=null;
public static synchronized Singleton2 getInstance(){
if(singleton==null){
singleton=new Singleton2();
}
return singleton;
}
}
运行结果:
3、饿汉式
是否 Lazy 初始化:否
是否多线程安全:是
实现难度:易
描述:这种方式比较常用,但容易产生垃圾对象。
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。
它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。
public class testSingleton3 {
public static void main(String[] args) {
Singleton3 t1 = Singleton3.getInstance();
Singleton3 t2 = Singleton3.getInstance();
System.out.println(t1==t2);
}
}
class Singleton3{
private Singleton3(){};
private static Singleton3 singleton=new Singleton3();
public static Singleton3 getInstance(){
return singleton;
}
}
运行结果:
4、双检锁/双重校验锁(DCL,即 double-checked locking)
JDK 版本:JDK1.5 起
是否 Lazy 初始化:是
是否多线程安全:是
实现难度:较复杂
描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
getInstance() 的性能对应用程序很关键。
/** volatile是什么,可以保证有序性吗? * 1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值, * 这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存中。 * 2)禁止进行指令重排序 * volatile不是原子性操作 * 保证部分有序性: * 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行 * 且结果已经对后面的操作可见;在其后面的操作肯定还没有进行; * 使用volatile一般用于状态标记量和单例模式的双检锁。 */
public class testSingleton4 {
public static void main(String[] args) {
Singleton4 t1 = Singleton4.getInstance();
Singleton4 t2 = Singleton4.getInstance();
System.out.println(t1==t2);
}
}
class Singleton4{
private Singleton4(){};
/** volatile是什么,可以保证有序性吗?
* 1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,
* 这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存中。
* 2)禁止进行指令重排序
* volatile不是原子性操作
* 保证部分有序性:
* 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行
* 且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
* 使用volatile一般用于状态标记量和单例模式的双检锁。
*/
private volatile static Singleton4 singleton;
public static Singleton4 getInstance(){
if(singleton==null){
synchronized (Singleton4.class){
if(singleton==null){
singleton=new Singleton4();
}
}
}
return singleton;
}
}
运行结果:
总结
单例模式的实现方法还有很多。上述是比较经典的实现方式,也是我们应该掌握的几种实现方式。
从这四种实现中,我们可以总结出,要想实现效率高的线程安全的单例,我们必须注意以下两点:
1.尽量减少同步块的作用域;
2.尽量使用细粒度的锁。