003 设计模式(Design Pattern)——单例模式

 

 

摘要:
  

       本文首先概述了单例模式产生动机,揭示了单例模式的本质和应用场景。紧接着,我们给出了单例模式在单线程环境下的两种经典实现:饿汉式 和 懒汉式,但是饿汉式是线程安全的,而懒汉式是非线程安全的。在多线程环境下,我们特别介绍了五种方式来在多线程环境下创建线程安全的单例,即分别使用 synchronized方法、synchronized块、静态内部类、双重检查模式 和 ThreadLocal 来实现懒汉式单例,并总结出实现效率高且线程安全的懒汉式单例所需要注意的事项。

一. 单例模式概述
  单例模式(Singleton),也叫单子模式,是一种常用的设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候,整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,显然,这种方式简化了在复杂环境下的配置管理。

  特别地,在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。事实上,这些应用都或多或少具有资源管理器的功能。例如,每台计算机可以有若干个打印机,但只能有一个 Printer Spooler (单例) ,以避免两个打印作业同时输出到打印机中。再比如,每台计算机可以有若干通信端口,系统应当集中(单例) 管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。

  综上所述,单例模式就是为确保一个类只有一个实例,并为整个系统提供一个全局访问点的一种方法。

二. 单例模式及其单线程环境下的经典实现
  单例模式应该是23种设计模式中最简单的一种模式了,下面我们从单例模式的定义、类型、结构和使用要素四个方面来介绍它。

1、单例模式理论基础

       确保一个类只有一个实例,JVM中一个类只会生成一个实例(对象)。,并为整个系统提供一个全局访问点 (向整个系统提供这个实例)。

三要素:

  • 私有的构造方法;

  • 指向自己实例的私有静态引用;

  • 以自己实例为返回值的静态的公有方法。

 

2、单线程环境下的两种经典实现

  在介绍单线程环境中单例模式的两种经典实现之前,我们有必要先解释一下 立即加载 和 延迟加载 两个概念。

立即加载 : 在类加载初始化的时候就主动创建实例;

延迟加载 : 等到真正使用的时候才去创建实例,不用时不去主动创建。

  在单线程环境下,单例模式根据实例化对象时机的不同,有两种经典的实现:一种是 饿汉式单例(立即加载),一种是 懒汉式单例(延迟加载)。饿汉式单例在单例类被加载时候,就实例化一个对象并交给自己的引用;而懒汉式单例只有在真正使用的时候才会实例化一个对象并交给自己的引用。代码示例分别如下:
饿汉式单例:类加载的方式是按需加载,且加载一次。因此,饿汉式单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用;而且,由于这个类在整个生命周期中只会被加载一次,因此只会创建一个实例,即能够充分保证单例。

package singleton.hunger;
/**
 * 饿汉式单例:立即加载
 * 饿汉式单例在单例类被加载时候,就实例化一个对象并交给自己的引用;
 * 无论单线程还是多线程都是线程安全;
 * @author Administrator
 *
 */
public class HungerSingleton {
	// 将自己作为私有的静态属性加载并实例化一个对象引用
	private static HungerSingleton hungerSigleton = new HungerSingleton();
	
	//私有化构造器杜绝外部创建
	private HungerSingleton() {
		
	}
	
	//静态方法返回自己创建的对象
	public static HungerSingleton getHungerSigleton() {
		
		return hungerSigleton;
	}
	
}

懒汉式单例:单例实例被延迟加载,即只有在真正使用的时候才会实例化一个对象并交给自己的引用。

package singleton.lazy;
/**
 * 初始的懒汉式:延迟加载
 * 懒汉式单例只有在真正使用的时候才会实例化一个对象并交给自己的引用。
 * @author Administrator
 *
 */
public class LazySingletonInital {
	// 指向自己实例的私有静态引用
	private static LazySingletonInital lazySigletonInital;
	// 私有的构造方法
	private LazySingletonInital() {
		
	}
	// 以自己实例为返回值的静态的公有方法
	public static LazySingletonInital getLazySigletonInital() {
		// 判断对象是否存在,不存在就创建
		if(lazySigletonInital == null) {
			lazySigletonInital = new LazySingletonInital();
		}
		//对象存在就返回已有对象
		return lazySigletonInital;
	}
}

总之,从速度和反应时间角度来讲,饿汉式(又称立即加载)要好一些;从资源利用效率上说,懒汉式(又称延迟加载)要好一些。

3、单例模式的优点

  我们从单例模式的定义和实现,可以知道单例模式具有以下几个优点:

  • 在内存中只有一个对象,节省内存空间;

  • 避免频繁的创建销毁对象,可以提高性能;

  • 避免对共享资源的多重占用,简化访问;

  • 为整个系统提供一个全局访问点。

4、单例模式的使用场景

  由于单例模式具有以上优点,并且形式上比较简单,所以是日常开发中用的比较多的一种设计模式,其核心在于为整个系统提供一个唯一的实例,其应用场景包括但不仅限于以下几种:

  • 有状态的工具类对象;
  • 频繁访问数据库或文件的对象;

5、单例模式的注意事项

  在使用单例模式时,必须使用单例类提供的公有工厂方法得到单例对象,而不应该使用反射来创建,否则将会实例化一个新对象。此外,在多线程环境下使用单例模式时,应特别注意线程安全问题。

三. 多线程环境下单例模式的实现
  在单线程环境下,无论是饿汉式单例还是懒汉式单例,它们都能够正常工作。但是,在多线程环境下,情形就发生了变化:由于饿汉式单例天生就是线程安全的,可以直接用于多线程而不会出现问题;但懒汉式单例本身是非线程安全的,因此就会出现多个实例的情况,与单例模式的初衷是相背离的。下面我重点阐述以下几个问题:

  • 为什么说饿汉式单例天生就是线程安全的?
  • 传统的懒汉式单例为什么是非线程安全的?
  • 怎么修改传统的懒汉式单例,使其线程变得安全?
  • 线程安全的单例的实现还有哪些,怎么实现?
  • 双重检查模式、Volatile关键字 在单例模式中的应用
  • ThreadLocal 在单例模式中的应用

1、为什么说饿汉式单例天生就是线程安全的?

       类加载的方式是按需加载,且只加载一次。因此,在饿汉式单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用,当线程访问单例对象之前就已经创建好了。而且,由于一个类在整个生命周期中只会被加载一次,因此该单例类只会创建一个实例,也就是说,线程每次都只能也必定只可以拿到这个唯一的对象。因此就说,饿汉式单例天生就是线程安全的。

public class TestSingleton {

	public static void main(String[] args) {
		
		Thread[] threads = new Thread[5];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new TestThread();
        }
 
        for (int i = 0; i < threads.length; i++) {
            threads[i].start();
        }
	}
}

class TestThread extends Thread {
    @Override
    public void run() {
    	//饿汉式
    	int hash = HungerSingleton.getHungerSigleton().hashCode(); 
        System.out.println(hash);
    }
}
/*输出结果
296685577
296685577
296685577
296685577
296685577
*/

2、传统的懒汉式单例为什么是非线程安全的?

public class TestSingleton {

	public static void main(String[] args) {
		
		Thread[] threads = new Thread[5];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new TestThread();
        }
 
        for (int i = 0; i < threads.length; i++) {
            threads[i].start();
        }
	}
}

class TestThread extends Thread {
    @Override
    public void run() {
    	
        //初始懒汉式
    	int hash = LazySingletonInital.getLazySigletonInital().hashCode(); 
        System.out.println(hash);
    }
}
/*输出结果不一致
474695494
296684173
296684173
296684173
313586000
*/

上面发生非线程安全的一个显著原因是,会有多个线程同时进入 if (singleton2 == null) {…} 语句块的情形发生。当这种这种情形发生后,该单例类就会创建出多个实例,违背单例模式的初衷。因此,传统的懒汉式单例是非线程安全的。

3、实现线程安全的懒汉式单例的几种正确姿势

1)、同步延迟加载 — synchronized方法

/**
 * 使用 synchronized 修饰 getLazySingletonSyn()方法,
 * 保证了对临界资源的同步互斥访问,也就保证了单例。
 * 但是在性能上会有所下降,因为每次调用getLazySingletonSyn(),都要上锁
 * 事实上,只有在第一次创建对象的时候需要加锁,这种实现方式的运行效率会很低。
 * @author Administrator
 *
 */
public class LazySingletonSyn {
	
	private static LazySingletonSyn lazySingletonSyn;

	private LazySingletonSyn() {
		
	}
	
    // 使用 synchronized 修饰方法,临界资源的同步互斥访问
	public static synchronized LazySingletonSyn getLazySingletonSyn() {
		if(lazySingletonSyn == null) {
			lazySingletonSyn = new LazySingletonSyn();
		}
		
		return lazySingletonSyn;
	}
	
}

        从执行结果上来看,问题已经解决了,但是这种实现方式的运行效率会很低,因为同步块的作用域有点大,而且锁的粒度有点粗。同步方法效率低,那我们考虑使用同步代码块来实现。
 

2)、同步延迟加载 — synchronized块 

public class LazySingletonSyn {
	
	private static LazySingletonSyn lazySingletonSyn;

	private LazySingletonSyn() {
		
	}
	
	// 使用 synchronized 代码块,临界资源的同步互斥访问
	public static LazySingletonSyn getLazySingletonSyn() {
		synchronized(LazySingletonSyn.class){
			if(lazySingletonSyn == null) {
				lazySingletonSyn = new LazySingletonSyn();
			}
		}
		return lazySingletonSyn;
	}
	 
}

       该实现与上面synchronized方法版本实现类似,此不赘述。从执行结果上来看,问题已经解决了,但是这种实现方式的运行效率仍然比较低,事实上,和使用synchronized方法的版本相比,基本没有任何效率上的提高。

3)、同步延迟加载 — 使用内部类实现延迟加载(推荐)

/**
 * 使用内部类实现线程安全的懒汉式单例,这种方式也是一种效率比较高的做法。
 * 线程安全同饿汉式单例天生就是线程安全道理一样。
 */
public class LazySingletonClass {
	
	private LazySingletonClass() {
		
	}
	/*
	 * 此处使用一个内部类来维护单例 JVM在类加载的时候,是互斥的,所以可以由此保证线程安全问题
	 */
	private static class LazyLoad {
		private static LazySingletonClass lazySingletonClass = new LazySingletonClass();
	}
	
	public static LazySingletonClass getLazySingletonClass() {
		return LazyLoad.lazySingletonClass;
	}

}

       这种方式跟饿汉式方式采用的机制类似,但又有不同。两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方在饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的作用,而静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化;类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的;避免了线程不安全,延迟加载,效率高。

四. 单例模式与双重检查(Double-Check idiom)

       使用双重检测同步延迟加载去创建单例的做法是一个非常优秀的做法,其不但保证了单例,而且切实提高了程序运行效率;

/** 线程安全的懒汉式单例
 * Double-Check:使用双重检测同步延迟加载去创建单例的做法是一个非常优秀的做法,其不但保证了单例,而且切实提高了程序运行效率。
 * volatile关键字的作用:
 * 1,可见性保证
 * 2,禁止指令重排序
 *
 */
public class LazySingletonSynDC {
	//使用volatile关键字防止重排序,因为 new LazySingletonSynDC()是一个非原子操作,可能创建一个不完整的实例
	private static volatile LazySingletonSynDC lazySingletonSynDC;
	
	private LazySingletonSynDC() {
		
	}
	
	public static LazySingletonSynDC getLazySingletonSynDC() {
		//Double-Check idiom
		if(lazySingletonSynDC ==null) {
			synchronized(LazySingletonSynDC.class) {
				if(lazySingletonSynDC == null) {
					lazySingletonSynDC = new LazySingletonSynDC();
				}
			}
		}
		return lazySingletonSynDC;
	}

}

如上述代码所示,为了在保证单例的前提下提高运行效率,需要对 lazySingletonSynDC进行第二次检查,目的是避开过多的同步(因为这里的同步只需在第一次创建实例时才同步,一旦创建成功,以后获取实例时就不需要同步获取锁了)。这种做法无疑是优秀的,但是必须注意一点:必须使用volatile关键字修饰单例引用

如果上述的实现没有使用 volatile 修饰 singleton3,会导致什么情形发生呢? 为解释该问题,我们分两步来阐述:

(1)、当我们写了 new 操作,JVM 到底会发生什么?

  首先,我们要明白的是: new Singleton3() 是一个非原子操作。代码行singleton3 = new Singleton3(); 的执行过程可以形象地用如下3行伪代码来表示:
 

memory = allocate();        //1:分配对象的内存空间
ctorInstance(memory);       //2:初始化对象
singleton3 = memory;        //3:使singleton3指向刚分配的内存地址

但实际上,这个过程可能发生无序写入(指令重排序),也就是说上面的3行指令可能会被重排序导致先执行第3行后执行第2行,也就是说其真实执行顺序可能是下面这种:

memory = allocate();        //1:分配对象的内存空间
singleton3 = memory;        //3:使singleton3指向刚分配的内存地址
ctorInstance(memory);       //2:初始化对象

这段伪代码演示的情况不仅是可能的,而且是一些 JIT 编译器上真实发生的现象。

(2)、重排序情景再现
  
  了解 new 操作是非原子的并且可能发生重排序这一事实后,我们回过头看使用 Double-Check idiom 的同步延迟加载的实现:

  我们需要重新考察上述清单中的 //3 行。此行代码创建了一个 Singleton 对象并初始化变量 singleton3 来引用此对象。这行代码存在的问题是,在 Singleton 构造函数体执行之前,变量 singleton3 可能提前成为非 null 的,即赋值语句在对象实例化之前调用,此时别的线程将得到的是一个不完整(未初始化)的对象,会导致系统崩溃。下面是程序可能的一组执行步骤:

  1、线程 1 进入 getSingleton3() 方法;
  2、由于 singleton3 为 null,线程 1 在 //1 处进入 synchronized 块;
  3、同样由于 singleton3 为 null,线程 1 直接前进到 //3 处,但在构造函数执行之前,使实例成为非 null,并且该实例是未初始化的;
  4、线程 1 被线程 2 预占;
  5、线程 2 检查实例是否为 null。因为实例不为 null,线程 2 得到一个不完整(未初始化)的 Singleton 对象;
  6、线程 2 被线程 1 预占。
  7、线程 1 通过运行 Singleton3 对象的构造函数来完成对该对象的初始化。

  显然,一旦我们的程序在执行过程中发生了上述情形,就会造成灾难性的后果,而这种安全隐患正是由于指令重排序的问题所导致的。让人兴奋地是,volatile 关键字正好可以完美解决了这个问题。也就是说,我们只需使用volatile关键字修饰单例引用就可以避免上述灾难。

五. 单例模式 与 ThreadLocal

       借助于 ThreadLocal,我们可以实现双重检查模式的变体。我们将临界资源instance线程私有化(局部化),具体到本例就是将双重检测的第一层检测条件 if (instance == null) 转换为线程局部范围内的操作,对应的代码清单如下:

public class Singleton {

    // ThreadLocal 线程局部变量,将单例instance线程私有化
    private static ThreadLocal<Singleton> threadlocal = new ThreadLocal<Singleton>();
    private static Singleton instance;

    private Singleton() {

    }

    public static Singleton getInstance() {

        // 第一次检查:若线程第一次访问,则进入if语句块;否则,若线程已经访问过,则直接返回ThreadLocal中的值
        if (threadlocal.get() == null) {
            synchronized (Singleton.class) {
                if (instance == null) {  // 第二次检查:该单例是否被创建
                    instance = new Singleton();
                }
            }
            threadlocal.set(instance); // 将单例放入ThreadLocal中
        }
        return threadlocal.get();
    }
}/* Output(完全一致): 
        1028355155
        1028355155
        1028355155
        1028355155
        1028355155
        1028355155
        1028355155
        1028355155
        1028355155
        1028355155
*///:~

借助于 ThreadLocal,可以实现线程安全的懒汉式单例。但与直接双重检查模式使用,本实现在效率上还不如后者。

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值