1. 概述
单例模式:简单的说就是可以确保只产生一个类实例,让多个用户或者多个线程同时使用这一个实例,而不需要每次使用都创建一次对象。
2. 优缺点和适用场景
单例模式节省了创建对象所需的时间,节约了系统资源,减轻了GC压力。
大部分的单例类的构造器是私有的,这就意味着单例类不容易扩展。
网上说单例的缺点还有就是长时间不使用,会被GC回收,导致对象状态的丢失,其实我不是很认同这点,我觉得单例是不会被GC回收的,毕竟是static类型的,而且始终指向着引用。这个以后具体查一查hotspot的有关实现
适用于需要频繁实例化然后销毁的类或者那些重量级对象,一次创建需要消耗很多系统资源。但是不适用于要保存状态的类,比如:一个订单类,用户A和用户B都有一条订单,A先从数据库中查出订单状态为已付款,这时订单类的状态就变成已付款了,然后B从数据库查出自己的订单状态是未付款的,如果这个订单类是单例的,那么A的订单状态也会变为未付款的,这就乱套了。
单例在多线程的场景中使用要格外小心,包括单例的创建以及共享资源的使用问题。
3. 几种不同形式的单例模式
- 饿汉式
class SingleClass{
private static SingleClass instance = new SingleClass();
private SingleClass() {
System.out.println("single");
}
public static SingleClass getInstance() {
return instance;
}
}
这种单例的实现方式非常简单而且可靠,不会涉及到在创建单例时的非线程安全问题,因为实例的创建是在类加载时完成的。但是当这种单例不光要承担创建实例的角色,又要完成其他工作的时候,就有点不那么得心应手了,比如:
class SingleClass{
private static SingleClass instance = new SingleClass();
private SingleClass() {
System.out.println("single");
}
public static SingleClass getInstance() {
return instance;
}
public static void doSomething(){
...
}
}
可以看到,这个单例不光要扮演创建实例的角色又要扮演其他角色(doSomething),当我们调用SingleClass.doSomething()的时候,如果这时类实例还没创建或者说类还没加载,虚拟机在这种场景下就会为我们加载类,并创建实例,然而我们这时并不想让SingleClass产生实例,因为还不需要用到SingleClass的实例。那么有没有一种方式可以延迟加载单例呢,让单例的创建能受我们控制,想让他什么时候创建就什么时候创建,而不是类一加载就创建实例。
- 懒汉式
class LazySingleClass{
private static LazySingleClass instance = null;
private LazySingleClass() {
System.out.println("LazySingleClass");
}
public static synchronized LazySingleClass getInstance() {
if( instance == null) {
instance = new LazySingleClass();
}
return instance;
}
public static void doSomething(){
...
}
}
当我们调用LazySingleClass.doSomething时,尽管虚拟机会加载类,但是不会创建类实例,因为我们把创建类实例的控制权完全交给了getInstance方法,只有我们调用getInstance时才会创建实例。虽然解决了延迟加载的问题,但是可以看到getInstance方法是加上了同步锁的,因为类实例不是在类加载时完成的,所以肯定涉及到非线程安全问题,当两个线程调用getInstance方法,如果不加上synchronized,一个线程创建完实例前,另一个线程判断instance是空的,这样很容易就创建了两个实例。
getInstance整个方法被加上了同步关键字,这样的效率是很低的,我们可以改良一下,把同步关键字就加在涉及线程安全问题的代码上
- DCL
class LazySingleClass2{
//这里volatile很重要
private volatile static LazySingleClass2 instance = null;
private LazySingleClass2() {
System.out.println("LazySingleClass2");
}
public static LazySingleClass2 getInstance() {
if( instance == null) {
synchronized(LazySingleClass2.class){
if( instance == null) {
instance = new LazySingleClass2();
}
}
}
return instance;
}
public static void doSomething(){
System.out.println("...");
}
}
双重检测,已经可以做到线程安全了,但要依赖JDK版本,在JDK5.0以后才适用,而且在效率上肯定是比不上饿汉式的。为了解决这个问题,还需要对单例模式进行改进。
- 使用内部类来实现单例
class InnerSingleClass{
private InnerSingleClass() {
System.out.println("InnerSingleClass");
}
private static class SingletonHolder{
private static InnerSingleClass instance = new InnerSingleClass();
}
public static InnerSingleClass getInstance() {
return SingletonHolder.instance;
}
public static void doSomething(){
System.out.println("...");
}
}
当外部类被加载时,内部类不会被初始化,而且将类实例的创建放在内部类加载时完成,避开了非线程安全问题。可以看到这种内部类的实现方式,既满足了延迟加载,又不涉及到非线程安全问题。
以上几种单例模式,的确在大多数情况下能够确保只产生一个实例了,但也有例外的情况,当通过反射,强行调用单例类的私有构造函数,就会产生多个实例,可以对私有构造函数进行异常检测。这种反射造成的问题是一种极端的方式,就不过多去讨论,还有一种情况就是序列化和反序列化的时候会破坏以上单例。
@Test
public void test6() throws IOException, ClassNotFoundException {
//InnerSingleClass的代码上面有,还有就是InnerSingleClass要继承Serializable接口
InnerSingleClass instance = InnerSingleClass.getInstance();
InnerSingleClass instance2 = null;
FileOutputStream fos = new FileOutputStream("single.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(instance);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("single.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
instance2 = (InnerSingleClass) ois.readObject();
System.out.println(instance == instance2);
}
这段程序先将InnerSingleClass序列化到文件single.txt,再把它从文件反序列化为对象,序列化前的对象和反序列化的对象理应是同一个对象,然而程序输出为false,事实证明,序列化和反序列化拿到的对象不是同一个单例,那么怎么来避免这一问题发生呢
class InnerSingleClass implements Serializable{
private InnerSingleClass() {
System.out.println("InnerSingleClass");
}
private static class SingletonHolder{
private static InnerSingleClass instance = new InnerSingleClass();
}
public static InnerSingleClass getInstance() {
return SingletonHolder.instance;
}
public static void doSomething(){
System.out.println("...");
}
//新加代码
public Object readResolve() {
return SingletonHolder.instance;
}
}
在单例类中新加方法readResolve就可以了,在反序列化时会自动调用readResolve方法。然而还有一种单例模式是支持反序列化的,即不用在单例类里加上readResolve方法
- 枚举实现单例
enum EnumAnimal{
INSTANCE;
private EnumAnimal() {
System.out.println("animal single");
}
}
这样就实现了一个动物类单例,它能保证在反序列化后也是单例的,并且是线程安全的,而且也能保证不被反射破坏,具体可以去看反射newInstance()的源码,讲的很清楚。这种方式是不可以实现延迟加载的。
class User{
}
enum EnumSingleClass{
INSTANCE;
private User user = null;
private EnumSingleClass() {
System.out.println("EnumSingleClass");
user = new User();
}
public User getInstance() {
return user;
}
}
这里用枚举类EnumSingleClass来实现User类的单例。当反序列化User时,发现单例被破坏了,这是毫无疑问的,又不是创建EnumSingleClass的单例,而是创建User的单例,要想反序列化后User单例不被破坏,只能在User中添加readResolve方法。