java设计模式之单例模式

单例模式是开发中使用最广的模式之一,单例对象的类必须保证内存中只有一个实例存在。整个应用中只需要拥有一个全局对象。确保某个类只有一个对象,避免产生多个对象消耗过多的资源,或者某种类型的对象只应该有一个,例如:创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源,这时就要考虑使用单例模式。

实现单例模式的关键点:
1. 构造方法不对外开放,一般为private。
2. 通过一个静态方法或者枚举返回单例类对象。
3. 确保单例类对象有且只有一个,尤其是在多线程环境下。
4. 确保单例类对象在反序列化时不会重新构建对象。


###1. 饿汉式:

特点:线程安全、调用效率高

	/**
	 * 
	 * 饿汉式
	 * 2017-1-13上午8:41:22
	 */
	public class Singleton {
		
		//  私有化构造方法,不让外界通过new创建对象
		private Singleton(){}
		
		//实例化静态对象
		private static Singleton instance = new Singleton();
		
		public static Singleton getInstance(){
			return instance;
		}
	}


这个能保证单例的关键在于java虚拟机加载class文件字节码到内存的时候只有一份,这个static类型的成员变量会随着类一起加载。也就保证了一份。


### 2. 懒汉式
特点:线程安全,但是调用效率不高,可以延迟加载


	/**
	 * 懒汉式
	 */
	public class Singleton {
		
		//  私有化构造方法,不让外界通过new创建对象
		private Singleton(){}
		
		//申明对象
		private static Singleton instance = null;
		
		public static synchronized Singleton getInstance(){
			if (instance == null) {
				instance = new Singleton();
			}
			return instance;
		}
	}

添加synchronized关键字,防止对象被多线程创建,例如,有A和B两个线程,当A访问时创建了对象,但是还没有赋值,这个时候cpu切换到B线程后,又会创建一个对象,这个时候就会有两个对象了,所以要添加synchronized关键字。

这个synchronized添加到这里后,非常影响代码的效率。在后边会给出这几种单例模式的执行时间。

### 3. DCL模式 (Double Check Lock) 双重检测锁模式

DCL方式实现单例模式的优点是在需要的时候才初始化,又能保证线程安全。比懒汉式优越。

	/**
	 * DCL 双重锁检测模式
	 */
	public class Singleton {
		
		//  私有化构造方法,不让外界通过new创建对象
		private Singleton(){}
		
		//实例化静态对象
		private static Singleton instance = null;
		
		public static Singleton getInstance(){
			if (instance == null) {
				synchronized (Singleton.class) {
					if (instance == null) {
						instance = new Singleton();
					}
				}
			}
			return instance;
		}
	}

可以看到getInstance方法对instance进行了两次判空,第一次判断为了避免不必要的同步,第二次判断为null才创建实例。

但是,上边的方式也会出现DCL失效的问题,由于java编译器为了提高效率,对指令进行重排的问题,会导致创建单例失效的问题。JDK1.5后可以使用volatile关键字解决这个问题。将instance的定义改成  privatevolatile static Singleton instance = null  就可以了。


###4. 静态内部类
特点:线程安全,效率高,可以延迟加载
	/**
	 * 静态内部类
	 */
	public class Singleton {
		//  私有化构造方法,不让外界通过new创建对象
		private Singleton(){}
		//实例化静态对象
		public static Singleton getInSingleton(){
			return SingletonHolder.instance;
		}
		
		//静态内部类
		private static class SingletonHolder{
			private static final Singleton instance = new Singleton();
		}
	}

当第一次加载Singleton类的时候不会去加载静态的内部类,只有在调用Singleton的getInstance方法的时候采会初始化内部类,创建对象。这种方式保证了线程安全,也保证对象的唯一性,也延迟了加载。

但是,在上面的几个单例模式中,在一个情况下它们会出现重新创建对象的情况,那就是反序列化。

通过序列化可以将一个单例的实例对象写到磁盘中,然后再读取回来,从而获取一个实例。即使构造方法是私有的,反序列化也可以创建一个新的实例,反序列化操作提供了一个特别的钩子方法,类中具有一个私有的、被实例化的方法readResolve(),这个方法可以让开发人员控制对象的反序列化。
在java中,单例是要求一个JVM中只有一个对象实例,而通过反序列化会产生一个新的对象,与原来的对象不是同一个对象。这样就会破坏单例模式。

解决方法,添加以下方法:

	private Object readResolve() throws ObjectStreamException{
		return instance;
	}

也就是在readResolve方法中将jinstance对象返回,而不是默认重新创建一个新的对象。
	public class Singleton {
		
		//  私有化构造方法,不让外界通过new创建对象
		private Singleton(){}
		
		//实例化静态对象
		private static Singleton instance = new Singleton();
		
		public static Singleton getInstance(){
			return instance;
		}
		/**
		 * 防止反序列化破坏单例模式
		 * @return
		 * @throws ObjectStreamException
		 */
		private Object readResolve() throws ObjectStreamException{
			return instance;
		}
	}

### 6. 使用容器管理单例类
	public class SingletonManager {
		private static Map<String, Object> objMap = new HashMap<String, Object>();
		//将多个单例注入到同一的管理类中
		public static void registerService(String key,Object instance){
			if (! objMap.containsKey(key)) {
				objMap.put(key, instance);
			}
		}
		//获取单例对象
		public static Object getService(String key){
			return objMap.get(key);
		}
	}

这种方式可以统一的管理所有的单例类,在使用时可以通过统一的接口进行获取操作,同时也降低了耦合度。在android 服务中就使用了这种方式管理所有的服务。
在我们调用Context的getSystemSerivce(String name)获取服务的时候,系统采用的就是将所有的单例注入到管理类中。

###  测试
对饿汉式进行测试
	public class Test {
		public static void main(String[] args) {
			Singleton singleton1 = Singleton.getInstance();
			Singleton singleton2 = Singleton.getInstance();
			System.out.println("singleton1 = " + singleton1);
			System.out.println("singleton2 = " + singleton2);
			try {
				
				Class<Singleton> clazz = (Class<Singleton>) Class.forName("xxx.Singleton");
				Constructor<Singleton> constructor = clazz.getDeclaredConstructor(null);
				constructor.setAccessible(true);
				Singleton newInstance1 = constructor.newInstance(null);
				Singleton newInstance2 = constructor.newInstance(null);
				System.out.println("newInstance1 = " +newInstance1);
				System.out.println("newInstance2 = " +newInstance2);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}

输出结果:
	singleton1 = com.lichen.Singleton@1d10a5c
	singleton2 = com.lichen.Singleton@1d10a5c
	newInstance1 = com.lichen.Singleton@ff2413
	newInstance2 = com.lichen.Singleton@9980d5

对其它几种方式的测试都是得到相同的结果,单例模式都被破坏了.说明反射会破坏单例模式。

解决办法:
在单例的构造方法中判断是否已经被实例化,如果已经实例化就抛出异常
	private Singleton(){
		if (instance != null) {
			throw new RuntimeException();
		}
	}

这样就可以保证反射不会破坏单例模式。

反序列化破坏单例模式:
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println("singleton1 = " + singleton1);
System.out.println("singleton2 = " + singleton2);
try {
	//写数据
	FileOutputStream fos = new FileOutputStream("d://data.txt");
	ObjectOutputStream oos = new ObjectOutputStream(fos);
	oos.writeObject(singleton1);
	oos.close();
	fos.close();
} catch (Exception e) {
	e.printStackTrace();
}

//读数据
InputStream in;
try {
	in = new FileInputStream("d://data.txt");
	ObjectInputStream ois = new ObjectInputStream(in);   
	Singleton readObject = (Singleton) ois.readObject();
	System.out.println("readObject = " +readObject);
	ois.close();
	in.close();
} catch (Exception e) {
	e.printStackTrace();
}

输出结果:
	singleton1 = com.lichen.Singleton@2acc65
	singleton2 = com.lichen.Singleton@2acc65
	readObject = com.lichen.Singleton@16acdd1

地址不同了,说明单例模式被破坏了。
所以为了防止单例模式被破坏,需要防止反射和反序列化破坏,在单例模式中添加代码:
	public class Singleton implements Serializable{
		
		//  私有化构造方法,不让外界通过new创建对象
		private Singleton(){
			if (instance != null) {
				throw new RuntimeException();
			}
		}
		
		//实例化静态对象
		private static Singleton instance = new Singleton();
		
		public static Singleton getInstance(){
			return instance;
		}
		/**
		 * 防止反序列化破坏单例模式
		 * @return
		 * @throws ObjectStreamException
		 */
		private Object readResolve() throws ObjectStreamException{
			return instance;
		}
	}

输出结果:
	singleton1 = com.lichen.Singleton@1d10a5c
	singleton2 = com.lichen.Singleton@1d10a5c
	readObject = com.lichen.Singleton@1d10a5c

## 效率比较
循环一亿次,测试

饿汉式: 79毫秒 ,可能在不同的机器上会有差异 。 
	public static void main(String[] args) {
		long start = System.currentTimeMillis();
		for (int i = 0; i < 100000000; i++) {
			Singleton singleton = Singleton.getInstance();
		}
		long end = System.currentTimeMillis();
		System.out.println("懒汉式消耗时间 = " + (end - start));
	}

懒汉式 :2172毫秒

DCL 双重锁检测模式 : 328毫秒

静态内部类: 31毫秒

从时间消耗上来说,静态内部类的效率是最高的。







1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看REAdMe.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看REAdMe.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看READme.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。
1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 、 1资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看READmE.文件(md如有),本项目仅用作交流学习参考,请切勿用于商业用途。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值