相信在设计模式中有一个经常提到的概念:单例模式,为什么它经常出现在面试话题中,因为它的应用场景十分广泛。

使用场景:

比如

  • 数据库连接池,作为数据库的缓存,避免频繁连接关闭数据库,
  • Java线程池,控制管理线程。
  • log4j日志记录,由始至终记录着运行日志。

定义:

保证系统中一个类只有一个实例,而且必须自己创建自己的唯一实例,该实例易于外界访问,从而方便对实例个数的控制并节约系统资源。


创建单例模式的几种方式以及比较

1. 饿汉模式

/*
 1. 饿汉模式:
 2. 优点:线程安全
 3. 缺陷:性能低/加载类就初始化单例/不适合需要外部传入参数配置的单例模式
 */
public class SingletonHungry {
	
	private static final SingletonHungry instance = new SingletonHungry();
	
	public static SingletonHungry getInstance() {
		return instance;
	}
		
	public static void main(String[] args) {		
		
		SingletonHungry s1 = SingletonHungry.getInstance();
		SingletonHungry s2 = SingletonHungry.getInstance();
		System.out.print("饿汉模式实例对比:");
		//true
		System.out.println(s1.getInstance()==s2.getInstance());
	}	
	
}

由于饿汉模式在类内部创建实例,所以它是线程安全,正式它在类内部就静态加载,所以它不能从外部传入参数配置。 具体来看看懒汉模式.

2. 懒汉模式

package com.dd.code.singleton;

/*
 * 懒汉模式
 * 优点:简单/对比饿汉,加载此单例可以外部传入配置
 * 缺陷:线程不安全
 */
public class SingletonLazy {

	private static SingletonLazy instance;
	
	/*	
	 * 配置成员conf(假设必须传入conf该单例才可以加载)
	 * 	这不能在类中优先初始化 private static final SingletonHungry instance = new SingletonHungry();
	 */
	private static String conf;
	
	
	//外部传入属性配置
	public static void setConf(String conf) {
		SingletonLazy.conf = conf;
	}
	
	public static SingletonLazy getInstance() {
		if (instance == null) {
			instance = new SingletonLazy();
		}
		return instance;
	}

	
	public static void main(String[] args) {
		SingletonLazy.setConf("配置文件优先");
		SingletonLazy s1 = SingletonLazy.getInstance();
		SingletonLazy s2 = SingletonLazy.getInstance();
		System.out.print("懒汉模式实例对比:");
		//true
		System.out.println(s1.getInstance()==s2.getInstance());
	}
	
}

对比饿汉模式,懒汉模式可以传入必要配置再手动实例化,但是由于手动实例化,则需要考虑线程安全问题。

3. 线程安全懒汉模式

/*
 *  线程安全懒汉加载
 *  优点:线程安全/可以外部传配置
 *  缺陷:代价较高,创建单例只需要第一次保证线程安全就好,不需要每次都同步
 *  优化解决:SingletonDoubleCheck
 */
public class SingletonThreadSafe {
	private static SingletonThreadSafe instance;
	
	//加了同步关键字synchronized
	private synchronized SingletonThreadSafe getInstance() {
		if (instance == null) {
			instance = new SingletonThreadSafe();
		}
		return instance;
	}
	
	public static void main(String[] args) {
		SingletonThreadSafe s1 = new SingletonThreadSafe();
		SingletonThreadSafe s2 = new SingletonThreadSafe();
		System.out.print("线程安全懒汉加载实例对比:");
		//true
		System.out.println(s1.getInstance()==s2.getInstance());
	}
}

加了同步关键字synchronized保证了线程安全,但是它的性能就降低了,而且其实创建单例只需要第一次保证线程安全就好,不需要每次都同步。

所以引入了新的优化,好像很厉害的双重锁检测模式

4. 双重锁检测单例


/*
 * 双重锁单例模式(较复杂)
 * 优点:可传入配置/对比SingletonThreadSafe性能优化,只有在实例为空(第一次实现同步)
 * 为什么要第二次判断instance是否为空,因为把synchronized放里层的话,
 * 有可能有多个线程进入了*临界区*,synchronized只能保证临界区每次由一个线程执行而已,
 * 二次检测可以让其他线程下次不初始化,防止冗余情况
 */
public class SingletonDoubleCheck {

	/*
	 * volatile关键字:
	 * 要知道,instance = new SingletonDoubleCheck();不是原子性操作,
	 * 虽然volatile关键字不能保证原子性,但是可以禁止指令重排
	 * 保证了instance = new SingletonDoubleCheck() 这一行的有效执行顺序 
	 */
	private volatile static SingletonDoubleCheck instance;
	
	
	private static SingletonDoubleCheck getInstance() {
		if (instance == null) {
			//非临界区
			synchronized (SingletonDoubleCheck.class) {
				//*临界区*
				if (instance == null) {
					instance = new SingletonDoubleCheck();
				}
			}
		}
		return instance;
	}
	
	public static void main(String[] args) {
		SingletonDoubleCheck s1,s2;
		s1 = SingletonDoubleCheck.getInstance();
		s2 = SingletonDoubleCheck.getInstance();
		System.out.print("双重锁单例加载实例对比:");
		//true
		System.out.println(s1==s2);
	}
	
}

双重锁模式可以说是性能和线程安全的折中,它保证线程安全又保证不需要每次都控制同步,第一个判断if (instance == null)用来拦截已经创建的线程。 主要复杂的是从非临界区到临界区的情况,即当未创建实例的时候:要知道synchronized关键字其实只能保证每次一个线程执行修饰代码块,并不能保证只有一个线程,假设有超过一个线程进入临界区,此时如果线程一执行instance = new SingletonDoubleCheck(),则线程2下一次会根据第二个if (instance == null)判断是否再次创建实例,所以第二个if其实相当于一个flag标记,它巧妙的避免了多个线程创建多个实例


这种方式有点烧脑,官方推荐还有另一种创建方式,静态内部类模式,比较推荐使用的一种

5. 静态内部类

/*
 * 静态内部类 
 */
public class SingletonNested {

	private static class SingletonHolder{
		public static final SingletonNested HOLDER_INSTANCE = new SingletonNested();
	}

	public static SingletonNested getInstance() {
		return SingletonHolder.HOLDER_INSTANCE;
	}
	
	public static void main(String[] args) {
		SingletonNested s1, s2;
		s1 = SingletonNested.getInstance();
		s2 = SingletonNested.getInstance();
		System.out.print("静态内部类实例对比:");
		//true
		System.out.println(s1==s2);
	}
}

静态内部类创建单例的优点:

  1. 由jvm保证线程安全,不需要用到同步控制,性能较高;
  2. 因为区别于懒加载把instance作为静态内部成员,所以类加载时不会实例化instance 只有getInstance调用才会初始化。

参考链接: