设计模式(一)—— 单例模式

Java中单例模式是一种应用非常广泛的设计模式,它主要用来保证java的某个类只有一个实例存在, 可以避免实例对象的重复创建,从而节约时间、空间,并且可以避免由于操作多个实例带来的逻辑错误。如果一个对象的使用贯穿整个应用程序,而且起到了全局统一管控的作用,那么单例模式也许是一种不错的选择。

单例模式虽然有多种写法,但大部分写法都有不足, 下面逐一介绍。

1. 饿汉模式

public class Singleton{
    private Singleton(){}
    private static Singleton singleton = new Singleton();
    public static Singleton getInstance() {
        return singleton;
    }
}

首先看到构造函数必须是private的,保证其他类不能直接实例化此类,而是通过静态方法返回一个静态实例;

饿汉模式是在类加载的时候就对实例进行创建,实例在整个程序周期都存在。它的好处是只有类加载的时候创建一次实例,不会存在多个线程创建多个实例的情况,避免了多线程同步问题。 但是它的问题也很明显,即使这个实例没有用到也会被创建,而且在类加载后就创建到了,内存就被浪费了。

这种实现方式适合单例占用内存比较小, 在初始化时就会被用到的情况。但是, 如果单例占用内存比较大,或单例只是在某个特定场景下用到,使用饿汉模式就不合适了,这时候就需要用到懒汉模式进行延迟加载。

2. 懒汉模式

public class Singleton {
    private Singleton(){}
    private static Singleton singleton;
    publice static getInstance() {
        if(singleton == null){
           singleton = new Singleton(); 
        }    
        return singleton;
    }
}

懒汉模式中的实例是在用到的时候才去创建,如果单例使用次数少,并创建实例消耗资源较多,那么可以选择懒汉模式;但是懒汉模式没有考虑线程安全问题,在多线程环境下, 可能会创建多个实例,因此需要加锁来解决线程同步问题。如下:

public class Singleton {
    private Singleton() {}
    private static Singleton singleton;
    private static synchronized Singleton getInstance() {
        if(singleton == null) {
            singleton = new Singleton();
        }    
        return singleton;
    }    
}

加锁后的懒汉模式解决了线程安全问题,但是它存在着性能问题,synchronized修饰的同步方法比一般方法要慢很多,下一个线程想要获取实例必须要等上一个线程释放锁后才可以,如果调用次数较多,就造成了较大的性能损耗。它也不够完美,不推荐用。因此有了双重校验锁的实现方式。

3. 双重校验锁

public class Singleton{
    private Singleton() {}
    private static Singleton singleton;
    public static Singleton getInstance() {
        if(singleton == null) {
            synchronized(Singleton.class) {
                if(singleton == null) { //2
                    singleton = new Singleton();    
                }            
            }
        }
        return singleton; 
    }    
}

可以看到上面的同步代码块外多了一层判空操作,由于单例对象只需要创建一次,如果后面再次调用getInstance()只需要直接返回实例对象。因此大部分情况下,都不会执行到同步代码块,从而提高了性能。

不过还需要考虑一种情况,假如两个线程A、B,A执行了if(singleton == null)语句, 它认为单例没有创建,此时B也执行到了同样语句, B也认为单例对象没有创建,然后两个线程依次执行同步代码块,并分别创建了一个单例对象。为了解决这个问题,我们还需要在同步代码块中增加一次判空操作,如上面代码块中的//2;

我们看到双重校验锁既实现了延迟加载,又解决了线程并发的问题,同时还解决了执行效率问题,是否就真的万无一失了呢?

这里要提到Java中的指令重排优化,所谓指令重排优化就是指在不改变原语意的情况下, 通过调整指令的执行顺序让程序运行的更快。JVM中没有规定编译器优化相关的内容,也就是说JVM可以自由的进行指令重排优化。

这个问题的关键就在指令重排的存在,会导致初始化Singleton和将对象地址赋给singleton字段的顺序不确定。在某个线程创建单例对象时, 在构造方法被调用之前,就为该对象分配了了内存空间并将对象的字段设置为默认值。此时就可能将分配的内存地址赋值给了singleton字段了,然而该对象还没有初始化。若紧接着另一个线程来调用getInstance(), 通过singleton字段取到的实例就不是正确的Singleton实例。

以上就是双重校验锁会失效的原因,不过还好在JDK1.5及以后版本增加了volatile关键字。volatile的一个语义就是禁止指令重排优化,也就保证了instance变量被赋值的时候对象已经是初始化过的,从而避免了上面说到的问题。增加了volatile关键字的代码如下:

public class Singleton{
    private Singleton() {}
    private static volatile Singleton singleton;
    public static Singleton getInstance() {
        if(singleton == null) {
            synchronized(Singleton.class) {
                if(singleton == null) { //2
                    singleton = new Singleton();    
                }            
            }
        }
        return singleton; 
    }    
}

以上就是完善的双重校验锁单例模式,也是比较推荐使用的一种。

4.静态内部类

public class Singleton{
    private Singleton() {}
    private static class SingletonHolder {
        public static Singleton singleton = new Singleton();
    }
    public static Singleton getInstance() {
        return SingletonHolder.singleton;
    }
}

这种方式利用类加载机制来保证只创建一个实例,与饿汉模式一样,也是利用类加载机制,因此不存在多线程并发问题。

不一样的是,它是在内部类里面去创建实例,这样的话,只要应用中不使用内部类,JVM就不会去加载这个类,也就不会创建单例对象,从而实现延迟加载。同时它的实现方式也比较简单,推荐使用。

5. 枚举

public enum Singleton {
    singleton;
    public void whateverMethod() {}
}

上面提到的四种实现单例方式都有共同的缺点:

(1)需要额外的工作来实现序列化,否则每次反序列化一个序列化的对象都会创建一个新的实例。

(2)可以使用反射强制调用私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候报错)。

而枚举类很好地解决了这两个问题,使用枚举除了线程安全和防止反射调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。但是实际工作中,很少见有人使用枚举单例。

为什么要用枚举实现单例模式(避免反射、序列化问题)

猜你喜欢

转载自blog.csdn.net/u012216131/article/details/84840545