你真的懂单例模式吗

单例模式是设计模式中使用最为普遍的模式之一,它是一种对象创建模式,用于产生一 个对象的具体实例,可以确保系统中一个类只产生一个实例。在Java语言中,这样的行为能 带来两大好处:

(1)对于频繁使用的对象,可以省去new操作花费的时间,这对于那些重量级对象而 言,是一笔非常可观的系统开销。

(2)由于new操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻GC压 力,缩短GC停顿时间。 因此对于系统的关键组件和被频繁使用的对象,使用单例模式可以有效地改善系统的性能。 单例模式的角色非常简单,只有单例类和使用者两个,如表2.1所示。

表2.1 单例模式的角色

图2.1 单例模式的结构 图2.1 单例模式的结构

单例模式的核心在于通过一个接口返回唯一的对象实例。一个简单的单例实现如下:

public class Singleton { private Singleton(){
System.out.println("Singleton is create"); //创建单例的过程可能会比较慢 } private static Singleton instance = new Singleton(); public static Singleton getInstance() { return instance;
}
}

注意代码中的重点标注部分,首先单例类必须要有一个private访问级别的构造函数,只 有这样,才能确保单例不会在系统的其他代码内被实例化,这一点是相当重要的。其次, instance成员变量和getInstance()方法必须是static的。 注意:单例模式是一种非常常用的结构,几乎所有的系统中都可以找到它的身影。 因此,希望读者通过本节的学习,了解单例模式的几种实现方式及各自的特点。 这种单例的实现方式非常简单,而且十分可靠,唯一的不足仅是无法对instance做延迟 加载。假如单例的创建过程很慢,而由于instance成员变量是static定义的,因此在JVM加载 单例类时,单例对象就会被建立,如果此时这个单例类在系统中还扮演其他角色,那么在任何使用这个单例类的地方都会初始化这个单例变量,而不管是否会被用到。例如,单例类作 为String工厂用于创建一些字符串(该类既用于创建单例,又用于创建String对象):

public class Singleton { private Singleton() { //创建单例的过程可能会比较慢 System.out.println("Singleton is create");
} private static Singleton instance = new Singleton(); public static Singleton getInstance() { return instance;
} public static void createString(){ //这是模拟单例类扮演其他角色 System.out.println("createString in Singleton");
}
}

当使用Singleton.createString()执行任务时,程序输出以下内容:


Singleton is create

createString in Singleton


可以看到,虽然此时并没有使用单例类,但它还是被创建出来,这也许是开发人员所不 愿意见到的。为了解决这个问题,并提高系统在相关函数调用时的反应速度,就需要引入延 迟加载机制。

public class LazySingleton { private LazySingleton(){ //创建单例的过程可能会比较慢 System.out.println("LazySingleton is create");
} private static LazySingleton instance = null; public static synchronized LazySingleton getInstance() { if (instance==null)
instance=new LazySingleton(); return instance;
}
}

首先,对于静态成员变量instance赋予初始值null,确保系统启动时没有额外的负载。其 次,在getInstance()工厂方法中,判断当前单例是否已经存在,若存在,则返回,不存在, 再建立单例。这里尤其要注意,getInstance()方法必须是同步的,否则在多线程环境下,当 线程1正新建单例完成赋值操作前,线程2可能判断instance为null,故线程2也将启动新建单 例的程序,从而导致多个实例被创建,因此同步关键字是必需步骤。 使用上例中的单例,虽然实现了延迟加载的功能,但和第一种方法相比,它引入了同步 关键字,因此在多线程环境中,它的时耗要远远大于第一种单例模式的时耗。以下测试代码 就说明了这个问题。

@Override public void run(){ for(int i=0;i<100000;i++)
Singleton.getInstance(); //LazySingleton.getInstance(); System.out.println("spend:"+(System.currentTimeMillis()-begintime));
}

开启5个线程同时完成以上代码的运行,使用第一种单例耗时0ms,而使用LazySingleton 却相对耗时约390ms,性能至少相差2个数量级.

为了使用延迟加载引入的同步关键字反而降低了系统性能,是不是有点得不偿失呢?为解决这个问题,还需要对其进行以下改进:

public class StaticSingleton { private StaticSingleton(){
System.out.println("StaticSingleton is create");
} private static class SingletonHolder { private static StaticSingleton instance = new StaticSingleton();
} public static StaticSingleton getInstance() { return SingletonHolder.instance;
}
}

在这个实现中,单例模式使用内部类来维护单例的实例,当StaticSingleton被加载时,其 内部类并不会被初始化,故可以确保当StaticSingleton类被载入JVM时不会初始化单例类,而 当getInstance()方法被调用时才会加载SingletonHolder,从而初始化instance。同时,由于实例 的建立是在类加载时完成的,故天生对多线程友好,getInstance()方法也不需要使用同步关 键字。因此,这种实现方式同时兼备以上两种实现方式的优点。

注意:使用内部类的方式实现单例,既可以做到延迟加载,也不必使用同步关键 字,是一种比较完善的实现方式。

通常情况下,用以上方式实现的单例已经可以确保在系统中只存在唯一实例了。但仍然 有例外情况——可能导致系统生成多个实例,例如在代码中通过反射机制,强行调用单例类 的私有构造函数生成多个单例。考虑到情况的特殊性,不对这种极端的方式进行讨论。 但仍有些合法的方法,可能导致系统出现多个单例类的实例。

以下是一个可以被串行化的单例:

public class SerSingleton implements java.io.Serializable{
String name; private SerSingleton() { //创建单例的过程可能会比较慢 System.out.println("Singleton is create");
name="SerSingleton";
} private static SerSingleton instance = new SerSingleton(); public static SerSingleton getInstance() { return instance;
} public static void createString(){
System.out.println("createString in Singleton");
} private Object readResolve(){ //阻止生成新的实例,总是返回当前对象 return instance;
}
}


测试代码

使用一段测试代码测试单例的串行化和反串行化,当去掉SerSingleton代码中加粗的 readResolve()函数时,测试代码抛出以下异常:


junit.framework.AssertionFailedError: expected:javatuning.ch2.singleton.serialization.SerSingleton@5224ee but was:javatuning.ch2.singleton.serialization.SerSingleton@18fe7c3


这说明测试代码中的s和s1指向了不同的实例,在反序列化后生成了多个对象实例。而 加上readResolve()函数的程序则正常退出,这说明即便经过反序列化,也仍然保持单例的特 征。事实上,在实现了私有的readResolve()方法后,readObject()方法已经形同虚设,它直接 使用readResolve()替换了原本的返回值,从而在形式上构造了单例。

注意:序列化和反序列化可能会破坏单例。一般来说,对单例进行序列化和反序列 化的场景并不多见,但如果存在,就要多加注意。

#java求职##Java##学习路径#
全部评论

相关推荐

牛客36400893...:我不是这个专业的,但是简历确实没有吸引我的亮点,而且废话太多没耐心看
0offer是寒冬太冷还...
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务