Java设计模式——单例模式

单例模式应该是日常开发中用得最多的设计模式了,它的思想就是保证在应用中一个类的实例只能有一个。

什么情形下需要用到单例模式?

在程序中我们经常会遇到有类似配置文件的需求,一般在整个应用中配置信息应该都是需要共享同一份的,这时可以利用单例模式,保证在程序中用到此配置类的实例时,都是同一个实例,保证程序运行的正确。
类似于这种情形还有很多,比如数据库,线程池等。针对这种共享的情形有人可能就会有疑问,那我把这些配置信息都设置为静态的成员变量不就得了,当然,这样做理论上没问题,也能保证程序中共享一份信息,但是静态变量的生命周期是跟随类的,比普通成员变量要长,所以对内存是很大的消耗,这也是单例模式最大的优点,能节省内存。

单例模式的几种写法

饿汉式:

public class SingleInstance {

    private static SingleInstance sInstance = new SingleInstance();

    private SingleInstance(){}

    public static SingleInstance getInstance(){
        return sInstance;
    }
}

可以看出这种写法比较暴力,在类加载的时候就初始化创建了一个实例,不管你需不需要使用都给你创建好了,所以称为饿汉式。这种写法的缺点就是不是懒加载,就算你不需要他也创建了,所以造成了一定的内存浪费。这种方式是线程安全的,可以正常使用,但不推荐

懒汉式1(线程不安全)

public class SingleInstance {

    private static SingleInstance sInstance;

    private SingleInstance(){}

    public static SingleInstance getInstance(){
        if(sInstance==null){
            sInstance = new SingleInstance();
        }
        return sInstance;
    }
}

这种方式是当你需要这个实例调用getInstance()的时候才会去创建,所以称为懒汉式。这种写法看起来是解决了饿汉式浪费内存的情况,但这种写法是线程不安全的

假设有一个线程调用了getInstance() 方法,判断sInstance等于null之后让出了cpu的执行权,此时另外一个线程拿到了cpu的执行权,而且也进入getInstance() 方法后判断sInstance==null也还是true,最终这两个线程就会创建出两个实例,是线程不安全的,在多线程中不可以使用。

既然存在线程不安全问题,那么就加个锁试试

懒汉式2(效率低)

public class SingleInstance {

    private static SingleInstance sInstance;

    private SingleInstance(){}

    public static synchronized SingleInstance getInstance(){
        if(sInstance==null){
            sInstance = new SingleInstance();
        }
        return sInstance;
    }
}

相对于前一种,在getInstance() 方法上加了一个锁,用来保证线程安全,但是这中写法效率太低。

假设线程A拿到了锁,进入了getInstance() 方法,但是方法还没执行完就让出了cpu执行权,此时另外一个线程也需要调用getInstance(),发现锁还没释放,于是就无法继续执行,得等线程A释放锁。但实际上,我们只需要保证在创建实例的时候线程安全就可以了,创建好之后判断sInstance==null为false,直接return就行,不会存在线程安全的问题,所以这种写法降低了程序执行的效率,不推荐使用。

既然这种效率低,是由于锁的范围太大,那换一个锁的位置试试

懒汉式3(线程不安全)

public class SingleInstance {

    private static SingleInstance sInstance;

    private SingleInstance(){}

    public static SingleInstance getInstance(){
        if(sInstance==null){
            synchronized(SingleInstance.class){
                sInstance = new SingleInstance();
            }
        }
        return sInstance;
    }
}

这种写法在实例已经创建好的情况下,会直接返回对象,不用等待锁,提高了运行效率。但是仍然存在线程不安全问题,当实例还没创建的情况下,同懒汉式1一样,当多个线程都判断if(sInstance==null)为true的情况,都会进入锁住的代码块内,最终创建出多个实例。因此这种写法也不可以使用

懒汉式4(双重校验锁DCL,推荐)

public class SingleInstance {简洁

    private static volatile SingleInstance sInstance;

    private SingleInstance(){}

    public static SingleInstance getInstance(){
        if(sInstance==null){
            synchronized(SingleInstance.class){
                if(sInstance==null){
                    sInstance = new SingleInstance();
                }
            }
        }
        return sInstance;
    }
}

分析懒汉式3,是因为当多个线程都能进入同步代码块时,会创建多个对象。所以改为在进入同步代码块之后,再判断一次,这样即使都先后进入了同步代码块,也不会多创建实例了,这样既解决了线程安全的问题,也解决了效率低的问题。这种双重校验的写法应该是平时用的最多的一种。

在双重校验锁方式中,还有个重要的地方做了改动,在声明sInstance时加了volatile关键字,之所以加入这个关键字是因为sInstance = new SingleInstance()这一行代码,不是原子操作。
在jvm中,这一行代码并不是一个操作完成的,而是大概会分成三个步骤去执行

  1. 给sInstance分配内存
  2. new操作,创建对象
  3. 将对象指向内存空间

其中第三步执行之后sInstance就不等于null了,由于还存在指令重排优化,可能会先执行第3步,再执行第2部,假设某个线程先执行了第3步,还没执行第二步,让出了cpu的执行权,此时另外一个线程判断sIntance已经不为null,则直接返回sIntance,但实际上sInstance还并没有创建真正的对象,最终就会导致程序出错。

总结一下这个问题的根源就是某个线程对sInstance的写操作还没完成,存在一个中间状态,此时让另外一个线程拿去进行了读操作,导致出现问题。

解决这个问题的办法就是使用volatile关键字,volatile会禁止指令重排,会保证在上述的三个操作执行完之后,才会让另外一个线程进行读操作,保障sInstance不会出现中间状态被读而产生错误的情况。

使用内部类

public class SingleInstance {

    private SingleInstance(){}

    private static class SingleInstanceHolder{
        private static SingleInstance sInstance = new SingleInstance();
    }

    public static SingleInstance getInstance(){
        return SingleInstanceHolder.sInstance;
    }
}

这种写法个人感觉就比较高级了,它主要利用了内部类的加载时机以及ClassLoader的同步机制保证单例的实现。

这种写法看起来类似于饿汉式的写法,但其实内部类要在外部类调用getInstance()方法使用到它时才会去加载,于是就变成了懒加载模式。其次ClassLoader在加载类的时候会保证只有一个线程来加载,也不会出现线程不安全的问题

使用枚举

public enum SingleInstance {

    sInstance;

}

这种方式写起来就相当暴力了,使用也是直接用SingleInstance.sInstance就可以。

根据枚举的特性,因为sInstance是SingleInstance的一个实例,而且只定义了一个sInstance,sInstance也不能被克隆,所以就保证了单例,同时因为创建枚举的过程是线程安全的,所以在多线程中使用也没有问题

另外枚举自身已经处理了序列化的问题,不会因为反序列化和反射产生多个实例的情况(这一块说实话还不是很了解原理,感觉也不是几句话能说清楚的,所以等深入研究后再做补充)

总结一下,在日常开发中用的比较多的应该就是双重校验锁的方式了,但是现在大牛们更推荐的是静态内部类和枚举的方式,特别是枚举,代码简洁,还能解决反序列化和反射引起的问题

以上就是对单例设计模式的一些理解和总结,如有不对的地方欢迎批评指正

推荐阅读更多精彩内容