JAVA设计模式-创建型模式-单例模式

本文深入解析单例模式的核心作用、应用场景及优缺点,探讨五种常见实现方式:饿汉式、懒汉式、双重检测锁式、静态内部类式与枚举式。同时,文章分析了如何选择合适的单例模式,以及如何破解单例模式并通过反射和序列化构造多个实例对象。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

核心作用

保证一个类只有一个实例,并且提供一个访问该实例的全局访问点。

常见应用场景

  1. Windows的Task Manager(任务管理器)就是很典型的单例模式
  2. windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
  3. 项目中,读取配置文件的类,一般也只有一个对象。没有必要每次使用配置文件数据,每次new一个对象去读取。
  4. 网站的计数器,一般也是采用单例模式实现,否则难以同步。
  5. 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
  6. 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。
  7. 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。
  8. Application 也是单例的典型应用(Servlet编程中会涉及到)
  9. 在Spring中,每个Bean默认就是单例的,这样做的优点是Spring容器可以管理
  10. 在servlet编程中,每个Servlet也是单例
  11. 在spring MVC框架/struts1框架中,控制器对象也是单例

单例模式的优点

  1. 由于单例模式只生成一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决
  2. 单例模式可以在系统设置全局的访问点,优化环共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理

常见的五种单例模式实现方式

主要
• 饿汉式(线程安全,调用效率高。 但是,不能延时加载。)
• 懒汉式(线程安全,调用效率不高。 但是,可以延时加载。)

其他
• 双重检测锁式(由于JVM底层内部模型原因,偶尔会出问题。不建议使用)
• 静态内部类式(线程安全,调用效率高。 但是,可以延时加载)
• 枚举单例(线程安全,调用效率高,不能延时加载)

饿汉式实现

public class Demo1 {

	//1.私有化构造器
	private Demo1(){}

	//2.构建静态私有实例
	private static final Demo1 instance = new Demo1();

	//3.获取静态公开的实例方法
	public static Demo1 getInstance(){
		return instance;
	}

}
  1. 饿汉式单例模式代码中,static变量会在类装载时初始化,此时也不会涉及多个线程对象访问该对象的问题。虚拟机保证只会装载一次该类,肯定不会发生并发访问的问题。
  2. 如果只是加载本类,而不是要调用getInstance(),甚至永远没有调用,则会造成资源浪费!

懒汉式实现

public class Demo2 {

	//1.私有化构造器
	private Demo2(){}

	//2.构建静态私有实例
	private static Demo2 instance = null;

	//3.获取静态公开的实例方法
	public static Demo2 getInstance(){
		//判断实例是否创建,未创建就创建实例
		if (null == instance) {
			instance = new Demo2();
		}
		return instance;
	}
	
}
  1. 延迟加载, 懒加载! 真正用的时候才加载!
  2. 资源利用率高了。

双重检测锁式实现

public class Demo3 {

	//1.私有化构造器
	private Demo3(){}

	//2.构建静态私有实例
	private static Demo3 instance = null;

	//3.获取静态公开的实例方法
	public static Demo3 getInstance(){
		//4.第一次检查
		if (null != instance) {
			return instance;
		}
		//5.添加同步锁
		synchronized (Demo3.class){
			//6.第二次检查
			if (null == instance){
				instance = new Demo3();
			}
		}
		return instance;
	}

}
  1. 这个模式将同步内容下方到if内部,提高了执行的效率不必每次获取对象时都进行同步,只有第一次才同步创建了以后就没必要了。

静态内部类式实现

public class Demo4 {

	//1.私有化构造器
	private Demo4(){}

	//2.构建静态内部类
	private static class LazyDemo4Instance {
		//3.在内部类构建Demo4的静态实例对象
		private static final Demo4 INSTANCE = new Demo4();
	}

	//4.构建获取实例静态公开方法
	public static Demo4 getInstance(){
		return LazyDemo4Instance.INSTANCE;
	}
	
}
  1. 静态内部类实现方式(也是一种懒加载方式)
  2. 外部类没有static属性,则不会像饿汉式那样立即加载对象只有真正调用getInstance(),才会加载静态内部类。加载类时是线程 安全的。 instance是static final类型,保证了内存中只有这样一个实例存在,而且只能被赋值一次,从而保证了线程安全性
  3. 兼备了并发高效调用和延迟加载的优势!

枚举式实现

public enum  Demo5 {

	INSTANCE;

}

优点:

  1. 实现简单
  2. 枚举本身就是单例模式。由JVM从根本上提供保障!避免通过反射和反序列化的漏洞!

缺点:

  1. 无延迟加载

如何选用

  1. 单例对象 占用 资源 少,不需要 延时加载:
    • 枚举式 好于 饿汉式
  2. 单例对象 占用 资源 大,需要 延时加载:
    • 静态内部类式 好于 懒汉式

破解单例模式

利用反射破解单例模式

反射可以破解上面几种(不包含枚举式)实现方式!

/**
	 * 用反射破解单例模式
	 */
	public static void reflect(){
		try {
			Demo3 one = Demo3.getInstance();
			//1.反射获取到Class对象
			Class clz = Class.forName("com.guhui.gof23.instance.Demo3");
			Constructor obj = clz.getDeclaredConstructor(null);
			//2.解开无参构造方法的私有限制,不然会报错
			obj.setAccessible(true);
			//3.获取实例
			Demo3 two = (Demo3) obj.newInstance();
			//打印两个实例
			System.out.println("双层检查锁单例-用反射破解创建多个实例-->Demo3-one:"+one);
			System.out.println("双层检查锁单例-用反射破解创建多个实例-->Demo3-two:"+two);
			//判断两个实例对象引用是否是同一个
			System.out.println(one==two);
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		} catch (InstantiationException e) {
			e.printStackTrace();
		} catch (NoSuchMethodException e) {
			e.printStackTrace();
		} catch (InvocationTargetException e) {
			e.printStackTrace();
		}
	}

虽然可以用反射破解单例模式,生成多个实例对象。不过可以在私有构造方法上,抛出异常,防止反射构造多个实例对象

//1.私有化构造器
	private Demo3(){
		if (instance != null){
			throw new RuntimeException();
		}
	}

在私有构造器里面判断,如果instance不为空,则抛出异常。达到防止反射构造多个实例对象

利用序列化构造多个实例对象

/**
	 * 用序列化和反序列化破解单例模式
	 */
	public static void serialize(){
		try {

			String path = "d:/demo3.text";

			Demo3 one = Demo3.getInstance();
			//1.序列化
			FileOutputStream fos = new FileOutputStream(path);
			ObjectOutputStream oos = new ObjectOutputStream(fos);
			oos.writeObject(one);
			oos.close();
			fos.close();

			//反序列化
			ObjectInputStream ois = new ObjectInputStream(new FileInputStream(path));
			Demo3 two = (Demo3) ois.readObject();


			//打印两个实例
			System.out.println("双层检查锁单例-序列化和反序列化破解单例-->Demo3-one:"+one);
			System.out.println("双层检查锁单例-序列化和反序列化破解单例-->Demo3-two:"+two);
			//判断两个实例对象引用是否是同一个
			System.out.println(one==two);
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
	}

同样,有通过序列化创建多个对象,就有办法可以防止创建多个实例对象

//反序列化时,如果定义了readResolve()则直接返回此方法指定的对象。而不需要单独再创建新对象!
	private Object readResolve() throws ObjectStreamException {
		return instance;
	}

在序列化类中,添加此方法。反序列化时,如果定义了readResolve()则直接返回此方法指定的对象。而不需要单独再创建新对象!

五种单例模式各自消耗时间。

public static void TestDemo1(){
		long start = System.currentTimeMillis();
		int threadNum = 10;
		final CountDownLatch countDownLatch = new CountDownLatch(threadNum);

		for(int i=0;i<threadNum;i++){
			new Thread(new Runnable() {
				@Override
				public void run() {

					for(int i=0;i<1000000;i++){
						Demo1.getInstance();
					}

					countDownLatch.countDown();
				}
			}).start();
		}

		try {
			countDownLatch.await();	//main线程阻塞,直到计数器变为0,才会继续往下执行!
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		long end = System.currentTimeMillis();
		System.out.println("饿汉式总耗时:"+(end-start));
	}

CountDownLatch

  1. 同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
  2. countDown() 当前线程调此方法,则计数减一(建议放在 finally里执行)
  3. await(), 调用此方法会一直阻塞当前线程,直到计时器的值为0

建立统计五个单例模式消耗时间的方法,打印时间如下

饿汉式总耗时:26
懒汉式总耗时:17
双层锁检查式总耗时:13
静态内类式总耗时:17
枚举式总耗时:20

此消耗时间以我本机为例,并不是绝对消耗时间。仅供参考

代码案例文件

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值