单例模式

核心作用:保证一个类只有一个实例,并提供一个访问该实例的全局访问点。

 

常见应用场景:

– Windows的Task Manager(任务管理器)就是很典型的单例模式

– windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。

– 项目中,读取配置文件的类,一般也只有一个对象。没有必要每次使用配置文件数据,每次new一个对象去读取。

– 网站的计数器,一般也是采用单例模式实现,否则难以同步。

– 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作

,否则内容不好追加。

– 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。

– 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。

– Application 也是单例的典型应用(Servlet编程中会涉及到)

– 在Spring中,每个Bean默认就是单例的,这样做的优点是Spring容器可以管理

– 在servlet编程中,每个Servlet也是单例

– 在spring MVC框架/struts1框架中,控制器对象也是单例

 

单例模式的5种实现:

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

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

双重检查锁式(由于JVM底层内部模型原因,偶尔会出问题。不建议使用)

静态内部类式(线程安全,调用效率高。 但是,可以延时加载)

枚举式(线程安全,调用效率高,不能延时加载)

 

单例模式的优点:

只生成一个实例,减少了系统性能开销。

 

单例模式的实现:

  1. 一个对象只创建一个实例
  2. 在系统设置该对象实例的全局访问点

1、饿汉式

  1. static变量会在类装载时初始化,虚拟机保证只会装载一次该类,而且此时也不会有多个线程访问的问题,所以线程安全
  2. 没有线程同步,所以调用效率高
  3. 由于在类装在时初始化,所有没有实现懒加载
public class SingletonDemo1 {

	//类初始化时加载,线程安全;但没有实现懒加载
	private static SingletonDemo1 singletonInstance = new SingletonDemo1();
	
	//私有化构造器
	private SingletonDemo1() {
	}
	
	//没有方法同步,调用效率高
	public static SingletonDemo1 getInstance() {
		return singletonInstance;
	}
	
}

2、懒汉式

  1. 使用synchronized线程同步,所以线程安全,只能创建一个实例。
  2. 使用了synchronized线程同步,所以执行效率低
  3. 没有在加载类的时候创建实例,而是在使用的时候才创建实例,实现了懒加载
public class SingletonDemo2 {

	private static SingletonDemo2 singletonInstance;
	
	//私有化构造器
	private SingletonDemo2() {
	}
	
	//方法同步,线程安全,调用效率低;初次调用方法时创建单例对象,实现懒加载
	public static synchronized SingletonDemo2 getInstance() {
		if(singletonInstance == null) {
			singletonInstance = new SingletonDemo2();
		}
		return singletonInstance;
	}
	
}

3、双重检查锁式

对懒汉式进行了优化,把synchronized放到了if语句下,不必每次获取对象都同步,只有第一次获取才同步。

问题:由于编译器优化原因和JVM底层内部模型原因,偶尔会出问题。不建议使用。

线程安全,执行效率高、实现了懒加载

public class SingletonDemo3 {

	private static SingletonDemo3 singletonInstance;
	
	//私有化构造器
	private SingletonDemo3() {
	}
	
	public static SingletonDemo3 getInstance() {
		if(singletonInstance == null) {
			SingletonDemo3 sc;
			synchronized (SingletonDemo3.class) {
				sc = singletonInstance;
				if(sc == null) {
					synchronized(SingletonDemo3.class) {
						if(sc == null) {
							sc = new SingletonDemo3();
						}
					}
					singletonInstance = sc;
				}
				
			}
		}
		return singletonInstance;
	}
	
}

4、静态内部类式

  1. 由于加载类SingletonDemo4时候,未加载内部类,执行getInstance()方法的时候才加载,所以实现了懒加载
  2. 加载内部类的时初始化,所以线程安全
  3. 没有同步,执行效率高
public class SingletonDemo4 {

	//SingletonDemo4初始化时不加载,实现懒加载
	private static class SingletonClassInstanc{
		//类初始化时加载,线程安全;
		private static final SingletonDemo4 singletonInstance = new SingletonDemo4();
	}
	
	//私有化构造器
	private SingletonDemo4() {
	}
	
	//方法没有同步,调用效率高
	public static SingletonDemo4 getInstance() {
		return SingletonClassInstanc.singletonInstance;
	}
	
}

5、枚举式

  1. 线程安全
  2. 没有同步,执行效率高
  3. 没有实现懒加载
  4. 枚举本身就是单例模式。由JVM从根本上提供保障!避免通过反射和反序列化的漏洞!
//JVM根本上提供保障,避免反射和反序列化漏洞
public enum SingletonDemo5 {

	//枚举元素,本身就是单例
	INSTANCE;
	
	public void SingletonOperation() {
		//单例的功能操作
	}
	
}

6、反射破解单例模式

反射可以破解上面的除枚举式以外的4四种单例实现,也就是说可以通过反射创建多个对象。

代码如下:

@Test
public void test() throws Exception {
		
	//创建并获得单例
	SingletonDemo6 s1 = SingletonDemo6.getInstance();
	System.out.println(s1);
		
	//通过反射直接调用私有构造器
	Class<SingletonDemo6> clazz = (Class<SingletonDemo6>) Class.forName("club.ityuchao.gof32.singleton.SingletonDemo6");
	Constructor<SingletonDemo6> constructor = clazz.getDeclaredConstructor(null);
	//跳过权限检查
	constructor.setAccessible(true);
	SingletonDemo6 s2 = constructor.newInstance();
	SingletonDemo6 s3 = constructor.newInstance();
	System.out.println(s2);
	System.out.println(s3);
}

避免通过反射创建多个对象的方法:

由于反射创建对象是走的构造方法,所以在构造方法中判断一下,如果在已经实例化的情况下依旧执行构造方法,就手动抛出异常。

代码如下(以懒汉式为例):

public class SingletonDemo6 implements Serializable {

	private static SingletonDemo6 singletonInstance;
	
	//私有化构造器
	private SingletonDemo6() {
		//防止反射破解单例模式
		if(singletonInstance != null) {
			throw new RuntimeException();
		}
	}
	
	//方法同步,线程安全,调用效率低;初次调用方法时创建单例对象,实现懒加载
	public static synchronized SingletonDemo6 getInstance() {
		if(singletonInstance == null) {
			singletonInstance = new SingletonDemo6();
		}
		return singletonInstance;
	}
	
}

7、反序列化破解单例模式

反序列化可以破解上面的除枚举式以外的4四种单例实现,也就是说可以通过反序列化创建多个对象。

代码如下:

@Test
public void test2() throws Exception {
	//创建并获得单例
	SingletonDemo6 s1 = SingletonDemo6.getInstance();
	System.out.println(s1);
		
	//通过反序列化构造多个对象
	FileOutputStream fos = new FileOutputStream("E://a.txt");
	ObjectOutputStream oos = new ObjectOutputStream(fos);
	oos.writeObject(s1);
	oos.close();
	fos.close();
		
	FileInputStream fis = new FileInputStream("E://a.txt");
	ObjectInputStream ois = new ObjectInputStream(fis);
	SingletonDemo6 s2 = (SingletonDemo6) ois.readObject();
	ois.close();
	fis.close();
		
	System.out.println(s2);
	
}

防止反序列化破解单例模式的方法:

让单例类实现Serializable 接口,在单例类中写一个readResolve()方法。

目的:反序列化的时候,如果定义了readResolve()方法,则直接此方法指定的对象,而不用创建新的对象

public class SingletonDemo6 implements Serializable {

	private static SingletonDemo6 singletonInstance;
	
	//私有化构造器
	private SingletonDemo6() {
		//防止反射破解单例模式
		if(singletonInstance != null) {
			throw new RuntimeException();
		}
	}
	
	//方法同步,线程安全,调用效率低;初次调用方法时创建单例对象,实现懒加载
	public static synchronized SingletonDemo6 getInstance() {
		if(singletonInstance == null) {
			singletonInstance = new SingletonDemo6();
		}
		return singletonInstance;
	}
	
	//反序列化的时候,如果定义了readResolve()方法,则直接此方法指定的对象,而不用创建新的对象
	private Object readResolve() throws ObjectStreamException{
		return singletonInstance;
	}
	
}

8、测试5种单例模式实现的效率

下面代码测试当执行10个线程,每个线程访问单例10万次执行的时间。

public class TestAll {

	public static void main(String[] args) throws InterruptedException {
		long start = System.currentTimeMillis();
		
		int threadNum = 10;
		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 < 100000; i++) {
						//Object o = SingletonDemo5.INSTANCE;
						Object o = SingletonDemo1.getInstance();
					}
					//计数器减一
					countDownLatch.countDown();
				}
			}).start();;
		}
		
		//阻塞mian线程,知道计数器为0,内部其实就是循环判断计数器是否为0
		countDownLatch.await();
		
		long end = System.currentTimeMillis();
		System.out.println("总耗时:" + (end - start));
	}
	
}

执行效果:

懒汉式:22

饿汉式:94

双重检查锁式:27

静态内部类式:23

枚举式:18

 

结论:就执行速度而言,饿汉式由于方法同步的原因,执行最慢,然后其他四种差不多速度。

9、如何选择

– 单例对象 占用 资源 少,不需要 延时加载:

• 枚举式 好于 饿汉式

– 单例对象 占用 资源 大,需要 延时加载:

• 静态内部类式 好于 懒汉式

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值