Java设计模式之一——单例模式

一、前言

单例模式(Singleton Pattern)是Java家族23种常用设计模式中使用最为普遍的模式之一,它是一种对象创建模式。该模式的作用是用于创建一个类的具体实例,通过该模式,可以确保系统中一个类只会产生一个实例。

二、使用单例模式的好处

在Java语言中,确保一个类只对应一个实例可以为使用Java开发的系统带来以下好处:

1、在系统中,对于频繁使用的对象,可以减少系统运行过程中创建对象所花费的时间,尤其是对于那些重量级对象而言,创建该对象将会是非常可观的一笔系统开销。

2、由于通过new来创建对象的次数减少,因而对系统内存的使用频率也会随之减少,带来的好处就是减轻GC(Gabage Collection)的压力,进而缩短GC的停顿时间,从而提高系统运行时的稳定性。

因此,对于系统中的关键组件以及需要被频繁使用的对象来说,用过使用单例模式将可以有效的改善系统性能。

三、单例模式涉及到的两个角色

1、单例类角色:作用是提供单例的工厂,返回单例。

2、使用者角色:作用是获取并且使用单例类的对象,相当于客户端。

四、单例模式的几种写法及其优缺点

1、 饥汉模式(通常也叫做饿汉模式

public class SingletonPattern {

	//构造方法私有
	private SingletonPattern(){
		
		System.out.println("Singleton is create");
	}
	
	//由于暴露获取单例对象的方法是static的,因此这里的instance成员变量需要声明为static
	private static SingletonPattern instance = new SingletonPattern();
	
	//暴露公共方法给外部直接通过类名调用
	public static SingletonPattern getInstance(){
		
		return instance;
	}
}

1.1、注意事项

1)、单例类必须要有一个private访问级别的构造方法,这样才能确保单例对象不会在系统中的其他代码内通过new关键字进行实例化。

2)、事例中的instance成员变量和getInstance()方法必须是static的,原因如代码注释。

1.2、优缺点

优点:实现简单,一般情况下创建的该单例对象都是可靠的。注意这里指一般情况下,后面会说在特殊情况下,尤其是多线程并发情况下,这种实现单例的方式创建的未必就是单例对象。

缺点:这种方法不能够对instance实例做延迟加载。假如这个单例的创建过程很缓慢,又由于成员变量instance是static的,这就会导致JVM在加载这个单例类时,该单例对象就会被创建。更有甚者,假如此时这个单例类整个系统中还扮演着其他角色的话,那么这个单例变量就会在任何使用这个单例类的地方都会被初始化,不管这个单例对象是否会被用到。如下例子:

public class SingletonPattern {

	//构造方法私有
	private SingletonPattern(){
		
		System.out.println("Singleton is create");
	}
	
	//由于暴露获取单例对象的方法是static的,因此这里的instance成员变量需要声明为static
	private static SingletonPattern instance = new SingletonPattern();
	
	//暴露公共方法给外部直接通过类名调用
	public static SingletonPattern getInstance(){
		
		return instance;
	}
	
	//模拟这个单例类还扮演着其他角色
	public static void getSomething(){
		
		System.out.println("getSomething method invoke!");
	}
	
	public static void main(String[] args) {
		
		getSomething();
	}
}

执行结果:


从如上结果可以看到,我们虽然此时并没有使用这个单例对象,但是它还是被创建出来了,这也许是开发人员不愿意见到的结果。

为了解决上面所说的问题,并且提高系统在相关方法调用时的效率,就引入了下面的饱汉模式(通常也叫做懒汉模式)

2、饱汉模式(也叫做懒汉模式)

public class LazySingletonPattern {

	private LazySingletonPattern(){
		
		System.out.println("LazySingletonPattern is create");
	}
	
	private static LazySingletonPattern instance = null;
	
	public static synchronized LazySingletonPattern getInstance(){
		
		if(null == instance){
			
			instance = new LazySingletonPattern();			
		}
		return instance;
	}
}

2.1、注意事项

1)、在声明成员变量instance的时候先赋值为null,这样就能在JVM加载这个单例类的时候没有额外的负载。

2)、在getInstance()方法中,先去判断单例是否已经存在,若已经存在则直接return返回该单例对象;不存在则创建后return返回。

3)、可能读者已经看到上面代码特殊的地方,即synchronized,这是Java的一个重要关键字。这样可以是getInstance()方法是线程同步的。否则在多线程的情况下,假如线程1先抢占到CPU资源真正新建单例时,在完成赋值给instance之前,线程2可能判断到instance是null,故线程2也会去执行创建单例的方法,这样就导致多个实例被创建,这样就违背了单例模式的初衷(确保单例对象只会存在一个)。因此这个同步关键字是必须的。

2.2、优缺点

优点:实现了instance实例的延迟加载,一定程度上提高了系统调用效率。并且实现了线程同步。

缺点:和饥汉模式相比,饱汉模式引入了同步关键字,因此在多线程环境下,它的时耗要远远大于饥汉模式。如下例子:

class MyThread extends Thread{
	
	@Override
	public void run() {
		
		long beginTime = System.currentTimeMillis();
		
		for(int i = 0; i < 100000; i++){
			
//			SingletonPattern.getInstance();
			
			LazySingletonPattern.getInstance();
			
			System.out.println("create singleton spend:" + (System.currentTimeMillis() - beginTime));
		}
	}
}

开启10个线程同时去执行以上代码,使用第1种类型的单例耗时1ms,而使用LazySingletonPattern却耗时410ms。性能上至少相差2个数量级(当然这里测试的结果会因为机器性能的不同而不同)

为了使用延迟加载,我们引入了同步关键字,但是这样做反而降低了系统的性能,是不是觉得有点得不偿失呢?至少我是这样认为的,O(∩_∩)O哈哈~。

为了解决这个性能问题,对上述代码继续进行了改进。

3、使用内部类来维护单例类的实例

public class InnerClassSingletonPattern {

	public InnerClassSingletonPattern() {

		System.out.println("InnerClassSingletonPattern is create");
	}
	
	//声明一个静态内部类
	private static class SingletonPatternHolder{
		//在静态内部类里面进行实例化单例对象
		private static InnerClassSingletonPattern instance = new InnerClassSingletonPattern();
	}
	
	public static InnerClassSingletonPattern getInstance(){
		
		return SingletonPatternHolder.instance;
	}
}

3.1、注意事项

需要声明一个静态的内部类,在该内部类中实例化单例对象。

3.2、优缺点

优点:在这个单例实现中,单例模式使用内部类来维护单例对象 ,当InnerClassSingletonPattern类被加载时,其内部类SingletonPatternHolder并不会被初始化,因此可以确保当InnerClassSingletonPattern类被JVM加载时,不会初始化单例类,只有当getInstance()方法被调用时,才会去加载SingletonPatternHolder,从而去初始化instance,即创建单例对象。同时,由于该单例实例的创建是在类加载时完成,故天生多线程友好,并且可以看到,getInstance()方法也不需要使用同步关键字了。

缺点:以上内部类的方法仍然有例外的情况,会导致系统生成多个实例。例如,在代码里面可以使用反射机制,强行调用单例类的私有构造方法,生成多个单例对象。但是这种方法属于是自己人为的制造这种特殊情况,在真正项目开发中,使用反射去强行调用某个类的私有构造方法的情况是不会发生的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值