[Java设计模式之单例模式]

  在Java程序中,要说用到的设计模式中,单例(Singleton)模式可能是使用最多的一种设计模式了。一些管理器和控制器常被设计成单例模式,Spring中, 一个Component就只有一个实例在Java-Web中, 一个Servlet类只有一个实例。
  Java
中单例(Singleton)模式是一种使用广泛的设计模式,单例模式的主要作用是保证在Java应用程序中,某个类只有一个实例存在。单例模式好处是它能够避免实例对象的重复创建,不仅可以减少每次创建对象的时间开销,还可以节约内存空间,并且能够避免由于操作多个实例导致的逻辑错误。如果一个对象有可能作用于整个应用程序,而且起到了全局统一控制管理的作用,那么单例模式是一个非常值得考虑的选择。

  单例模式的写法有很多种,其中常用的写法就是饿汉模式、懒汉模式及对象序列化模式。在介绍单例模式的设计前,先简单介绍静态内部类的加载机制。一些写法可能不足,欢迎大家多多指教。

静态内部类加载机制

    Java中静态内部类的加载机制,代码如下:

/**
 * 内部类的加载机制测试
 */
public class InnerClassLoadTest {
	static {
	    System.out.println("Load outer class,className:" + InnerClassLoadTest.class.getName());
	}

	public void doThing() {
	    System.out.println("Call outer class method,methodName:doThing()");
	}

	/**
	 * 定义内部静态类
	 */
	static class InnerClass {
		static {
			System.out.println("Load static inner class,className:" + InnerClass.class.getName());
		}

		static void doThing() {
			System.out.println("Call static inner class method,methodName:doThing()");
		}
	}

	public static void main(String[] args) {
		// 测试:此刻其内部类是否也会被加载?
		InnerClassLoadTest outerTest = new InnerClassLoadTest();
		outerTest.doThing();
		System.out.println("***************************调用静态内部类的的静态方法***************************");
		// 调用内部类的静态方法,此时才会初始化静态内部类
		InnerClassLoadTest.InnerClass.doThing();
	}

	// 运行结果:
	// Load outer class,className:com.innerclass.loader.InnerClassLoadTest
	// Call outer class method,methodName:doThing()
	// ***************************调用静态内部类的的静态方法***************************
	// Load static inner
	// class,className:com.innerclass.loader.InnerClassLoadTest$InnerClass
	// Call static inner class method,methodName:doThing()

	// 结论:
	// (1) 加载一个类时,其内部静态类不会同时被加载
	// (2) 一个类被加载,当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时发生
}

    了解Java的内部静态类加载机制,对于后面了解懒汉式单例模式有极大的帮助。

饿汉单例模式

    代码如下:

/**
 * 单例设计模式-饿汉式
 * 
 * 优点:线程安全
 * 
 * 缺点:很明显,类加载的时候就实例化对象了,浪费空间
 */
public class HungrySingletonDemo {
	private static HungrySingletonDemo instance = new HungrySingletonDemo();

	public static HungrySingletonDemo getInstance() {
		return instance;
	}

	private HungrySingletonDemo() {
		System.out.println("instantiation hungry singleton");
	}

	public void doThing() {
		System.out.println("this is a hungry singleton demo");
	}

	public static void main(String[] args) {
		HungrySingletonDemo.getInstance().doThing();
	}

	// 运行结果:
	// instantiation hungry singleton
	// this is a hungry singleton demo
}

   这种实现方式是线程安全的,但是确定也很明显,类加载的时候就实例了对象,浪费空间。这种实现方式适合单例占用内存比较小,在初始化时就会被用到的情况。如果单例占用的内存比较大,或者单例在某个特定场景下才会用到,使用饿汉模式就不合适了,这时候就需要用到懒汉模式进行延迟加载。

懒汉单例模式

懒汉单例之延迟加载模式

代码如下:

/**
 * 单例设计模式-懒汉式
 * 
 * 缺点:线程不安全
 * 
 */
public class LazySingletonDemo {
	private static LazySingletonDemo instance;

	public static LazySingletonDemo getInstance() {
		if (instance == null) {
			instance = new LazySingletonDemo();
		}
		return instance;
	}

	private LazySingletonDemo() {
		System.out.println("instantiation lazy singleton");
	}

	public void doThing() {
		System.out.println("this is a lazy singleton demo");
	}

	public static void main(String[] args) {
		LazySingletonDemo.getInstance().doThing();
	}
}

    这种写法,虽然实现了延迟加载,但是却不是线程安全的,在大量的并发场合下,可能会创建多个实例。为了实现线程安全,就在getInstance()方法内加载synchronized修饰符,于是就有了线程安全的懒汉单例模式。

懒汉单例模式之线程安全

代码如下:

/**
 * 单例设计模式-线程安全的懒汉式
 * 
 * 缺点:实例化前使用synchronized进行线程锁安全,性能上会大打折扣,因为syncrhonized会造成线程阻塞
 * 
 */
public class LazySingletonSynchronizedDemo {
	private static LazySingletonSynchronizedDemo instance;

	public static LazySingletonSynchronizedDemo getInstance() {
		synchronized (LazySingletonSynchronizedDemo.class) {
			if (instance == null) {
				instance = new LazySingletonSynchronizedDemo();
			}
		}
		return instance;
	}

	private LazySingletonSynchronizedDemo() {
		System.out.println("instantiation synchronized lazy singleton");
	}

	public void doThing() {
		System.out.println("this is a synchronized lazy singleton demo");
	}

	public static void main(String[] args) {
		LazySingletonSynchronizedDemo.getInstance().doThing();
	}
}

    此写法的优点是实现了线程安全,但实例化前使用synchronized进行线程锁安全,性能上会大打折扣,因为syncrhonized会造成线程阻塞。为了进一步提高单例模式的性能,于是提出了高性能的线程安全模式。

懒汉单例之双重校验锁模式

代码如下:

/**
 * 单例设计模式-双重校验锁的线程安全的懒汉式
 * 
 * 优点:确保了线程安全,还提高了性能
 */
public class LazySingletonDoubleCheckDemo {
	private static LazySingletonDoubleCheckDemo instance;

	public static LazySingletonDoubleCheckDemo getInstance() {
		if (instance == null) {
			synchronized (LazySingletonDoubleCheckDemo.class) {
				if (instance == null) {
					instance = new LazySingletonDoubleCheckDemo();
				}
			}
		}
		return instance;
	}

	private LazySingletonDoubleCheckDemo() {
		System.out.println("instantiation double check lazy singleton");
	}

	public void doThing() {
		System.out.println("this is a double check lazy singleton demo");
	}

	public static void main(String[] args) {
		LazySingletonDoubleCheckDemo.getInstance().doThing();
	}
}
     使用双重校验锁的线程安全的懒汉式,优点很明显,确保了线程安全,还提高了性能。

    但是,这种模式依然不能保证系统的单例模式的实现,在使用反射机制仍然可以创建对象的多个实例。测试代码如下:

/**
 * 懒汉式单例设计模式反射加载测试
 */
public class ReflexLoadLazySingletonTest {
	public static void main(String[] args) {
		// 创建第一个实例
		LazySingletonDoubleCheckDemo lazyInstance1 = LazySingletonDoubleCheckDemo.getInstance();
		// 通过反射创建第二个实例
		LazySingletonDoubleCheckDemo lazyInstance2 = null;
		try {
			// 使用反射可以创建多个实例
			Class clazz = LazySingletonDoubleCheckDemo.class;
			Constructor cons = clazz.getDeclaredConstructor();
			cons.setAccessible(true);
			lazyInstance2 = cons.newInstance();
		} catch (Exception e) {
			e.printStackTrace();
		}
		// 检查两个实例的hash值
		System.out.println("lazyinstance1 hashcode:" + lazyInstance1.hashCode());
		System.out.println("lazyinstance2 hashcode:" + lazyInstance2.hashCode());
		System.out.println(
				"lazyinstance1 equals lazyInstance2:" + (lazyInstance1.hashCode() == lazyInstance2.hashCode()));
	}
	// 运行结果:
	// instantiation double check lazy singleton
	// instantiation double check lazy singleton
	// lazyinstance1 hashcode:292938459
	// lazyinstance2 hashcode:917142466
	// lazyinstance1 equals lazyInstance2:false
	//
	// 结论:
	// 通过反射可以创建多个懒汉单例实例,单例结构模式被破坏了
}

    从测试代码中我们可以看出,使用Java的反射技术可以创建对象的多个实例,这在我们的实际生产环境中,可能不能满足实际需求的功能实现。

   在前面的介绍中,介绍了静态内部类的加载机制,其特点是:加载一个类时,其内部静态类不会同时被加载,并且一个类被加载,当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时发生。于是,使用静态内部类的加载机制,提出了另外一种设计饿汉模式。

饿汉模式之静态内部类加载

代码如下:

/**
 * 单例模式:使用内部静态类构建懒汉式单例
 * 对象实例化是在内部类加载的时候构建的,可见此版本的懒汉单例模式是线程安全的,
 * 因为在方法中创建对象,才存在并发问题,静态内部类随着方法调用而被加载,且只加载一次,不存在并发问题,所以是线程安全的。
* 另外,在getInstance()方法中没有使用synchronized关键字,没有加上synchronized从而造成多余的性能损耗。
* 当LazySingletonInnerClassDemo类加载的时候,其静态内部类LazyHolder并没有被加载,因此instance对象并没有构建。
 * 而我们在调用LazySingletonInnerClassDemo.getInstance()方法时,内部类LazyHolder被加载,此时单例对象才被构建。
* 因此,这种写法节约内存占用空间,达到懒加载的目的。
 */
public class LazySingletonInnerClassDemo {
	/**
	 * 定义内部静态类
	 */
	static class LazyHolder {
		private static final LazySingletonInnerClassDemo instance = new LazySingletonInnerClassDemo();
		static {
			System.out.println("Load lazy holder class,className:" + LazyHolder.class.getName());
		}
	}

	public static LazySingletonInnerClassDemo getInstance() {
		return LazyHolder.instance;
	}

	private LazySingletonInnerClassDemo() {
		System.out.println("Init lazy singleton inner class,className:" + LazySingletonInnerClassDemo.class.getName()
				+ ",hashcode:" + hashCode());
	}

	public static void main(String[] args) {
		// 创建第一个实例
		LazySingletonInnerClassDemo instance1 = LazySingletonInnerClassDemo.getInstance();
		// 通过反射创建第二个实例
		LazySingletonInnerClassDemo instance2 = null;
		try {
			Class clazz = LazySingletonInnerClassDemo.class;
			Constructor cons = clazz.getDeclaredConstructor();
			cons.setAccessible(true);
			instance2 = cons.newInstance();
		} catch (Exception ex) {
			ex.printStackTrace();
		}
		// 检查两个实例的hash值
		System.out.println("Instance1 hashcode:" + instance1.hashCode());
		System.out.println("Instance2 hashcode:" + instance2.hashCode());
	}

	// 运行结果:
	// Init lazy singleton inner
	// class,className:com.singleton.lazyinnerclass.LazySingletonInnerClassDemo,hashcode:292938459
	// Load lazy holder
	// class,className:com.singleton.lazyinnerclass.LazySingletonInnerClassDemo$LazyHolder
	// Init lazy singleton inner
	// class,className:com.singleton.lazyinnerclass.LazySingletonInnerClassDemo,hashcode:917142466
	// Instance1 hashcode:292938459
	// Instance2 hashcode:917142466
	//
	// 结论:
	// 使用反射机制创建实例时,单例结构依然被破坏,无法在系统全局中构建唯一的单例实例
}

    这种设计模式,依然没有办法保证整个系统中实现对象唯一的创建一个实例模式。于是提出了对这种设置模式进行了保护作用,从而达到了在系统中创建对象的唯一的一个实例模式。

代码如下:

/**
 * 单例模式:使用内部静态类构建懒汉式单例,保护单例使得系统始终只能创建一个单例
 * 
 * 缺点:多次创建实例的时候,程序会抛出异常
 */
public class LazySingletonInnerClassProtectDemo {
	private static boolean isInit = false;

	/**
	 * 定义内部静态类
	 */
	static class LazyHolder {
		private static final LazySingletonInnerClassProtectDemo instance = new LazySingletonInnerClassProtectDemo();
		static {
			System.out.println("Load lazy holder class,className:" + LazyHolder.class.getName());
		}
	}

	public static LazySingletonInnerClassProtectDemo getInstance() {
		return LazyHolder.instance;
	}

	private LazySingletonInnerClassProtectDemo() {
		if (isInit == false) {
			isInit = true;
		} else {
			throw new RuntimeException("The singleton has initialize,can't construct again.");
		}
		System.out.println("Init lazy singleton inner class,className:" + LazySingletonInnerClassDemo.class.getName()
				+ ",hashcode:" + hashCode());
	}

	public static void main(String[] args) {
		// 创建第一个实例
		LazySingletonInnerClassProtectDemo instance1 = LazySingletonInnerClassProtectDemo.getInstance();
		// 通过反射创建第二个实例
		LazySingletonInnerClassProtectDemo instance2 = null;
		try {
			Class clazz = LazySingletonInnerClassProtectDemo.class;
			Constructor cons = clazz.getDeclaredConstructor();
			cons.setAccessible(true);
			instance2 = cons.newInstance();
		} catch (Exception ex) {
			ex.printStackTrace();
		}
		// 检查两个实例的hash值
		System.out.println("Instance1 hashcode:" + instance1.hashCode());
		if (instance2 != null) {
			System.out.println("Instance2 hashcode:" + instance2.hashCode());
		}
	}

	// 运行结果:
	// Init lazy singleton inner
	// class,className:com.singleton.lazyinnerclass.LazySingletonInnerClassDemo,hashcode:292938459
	// Load lazy holder
	// class,className:com.singleton.lazyinnerclass.LazySingletonInnerClassProtectDemo$LazyHolder
	// java.lang.reflect.InvocationTargetException
	// at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	// at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
	// at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
	// at java.lang.reflect.Constructor.newInstance(Unknown Source)
	// at
	// com.singleton.lazyinnerclass.LazySingletonInnerClassProtectDemo.main(LazySingletonInnerClassProtectDemo.java:41)
	// Caused by: java.lang.RuntimeException: The singleton has initialize,can't
	// construct again.
	// at
	// com.singleton.lazyinnerclass.LazySingletonInnerClassProtectDemo.(LazySingletonInnerClassProtectDemo.java:26)
	// ... 5 more
	// Instance1 hashcode:292938459
	//
	// 结论
	// 使用反射无法破坏单例结构,即阻止了使用反射去创建单例的实例,使得系统全局中始终只有一个单例实例在运行
}

     到这里,已经介绍了单例设计模式中常用的设计方法,这基本上满足了日常需求功能开发的要求了。但是,在某些情况下需要在单例类中实现 Serializable 接口,这样就可以在文件系统中存储它的状态。

饿汉模式之反序列化模式

代码如下:


**
 * 单例模式:使用序列化和反序列化创建单例模式
 * 
 * 通过实现 Serializable接口,能保证了系统中始终创建一个单例实例
 */
public class LazySingletonSerializeDemo implements Serializable {
	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	private static boolean isInit = false;

	/**
	 * 定义内部静态类
	 */
	static class LazyHolder {
		private static final LazySingletonSerializeDemo instance = new LazySingletonSerializeDemo();
		static {
			System.out.println("Load lazy holder class,className:" + LazyHolder.class.getName());
		}
	}

	public static LazySingletonSerializeDemo getInstance() {
		return LazyHolder.instance;
	}

	private LazySingletonSerializeDemo() {
		if (isInit == false) {
			isInit = true;
		} else {
			throw new RuntimeException("The singleton has initialize,can't construct again.");
		}
		System.out.println("Init lazy singleton inner class,className:" + LazySingletonSerializeDemo.class.getName()
				+ ",hashcode:" + hashCode());
	}

	/**
	 * readResolve()代替了从流中读取对象。这就确保了在序列化和反序列化的过程中没人可以创建新的实例
	 */
	private Object readResolve() {
		return getInstance();
	}

	public static void main(String[] args) {
		// 创建第一个实例
		LazySingletonSerializeDemo instance1 = LazySingletonSerializeDemo.getInstance();
		ObjectOutput output = null;
		ObjectInput input = null;
		try {
			// 通过序列化创建第二个实例
			output = new ObjectOutputStream(new FileOutputStream("LazySingletonSerializeDemo"));
			output.writeObject(instance1);
			input = new ObjectInputStream(new FileInputStream("LazySingletonSerializeDemo"));
			LazySingletonSerializeDemo instance2 = (LazySingletonSerializeDemo) input.readObject();
			System.out.println("Instance1 hashCode=" + instance1.hashCode());
			System.out.println("Instance2 hashCode=" + instance2.hashCode());
		} catch (Exception ex) {
			ex.printStackTrace();
		} finally {
			if (output != null) {
				try {
					output.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			if (input != null) {
				try {
					input.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}

	// 运行结果:
	// Init lazy singleton inner
	// class,className:com.singleton.Serializable.LazySingletonSerializeDemo,hashcode:292938459
	// Load lazy holder
	// class,className:com.singleton.Serializable.LazySingletonSerializeDemo$LazyHolder
	// Instance1 hashCode=292938459
	// Instance2 hashCode=292938459
	//
	// 结论:
	// 使用了序列化方式能保证了对象的一致性,依然保持了单例结构
}

从测试结果中可以看出,使用序列化和反序列化模式,可是实现保证了在系统全局中实现了对象只能创建一个实例。

总结

  本文总结了多种Java中实现单例的方法,其中前两种都不够完美,双重校验锁和静态内部类的方式可以解决大部分问题,平时工作中使用的最多的也是这两种方式。反序列化实现方式虽然很完美的解决了各种问题,但是这种写法让人感觉有些生疏,实际使用中用到的较少。在没有特殊需求的情况下,双重校验锁和静态内部类的方式实现单例模式就已经足够了。


你可能感兴趣的:(单例模式,Java)