一、前言
单例模式(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()方法也不需要使用同步关键字了。
缺点:以上内部类的方法仍然有例外的情况,会导致系统生成多个实例。例如,在代码里面可以使用反射机制,强行调用单例类的私有构造方法,生成多个单例对象。但是这种方法属于是自己人为的制造这种特殊情况,在真正项目开发中,使用反射去强行调用某个类的私有构造方法的情况是不会发生的。