单例模式,是设计模式中比较简单,常用的一种。它有很多种写法,各有优劣,并涉及到多线程的情况下的线程安全问题,有些同学可能对单例还是一知半解,所以今天来聊一聊单例模式。
什么是单例模式?
保证一个类仅有一个实例,并提供一个访问它的全局访问点。通过使用单例模式,可以节约系统的资源开销,避免共享资源的多重占用等优点。
我什么时候会用到单例?
- 对于创建对象需要消耗很多资源的对象。如:数据库连接池对象,线程池对象等
- 只需要一个对象保证全局的一致性的。如:网站的计数器等
先实现一个单例模式
public class Singleton {
private Singleton singleton;
private Singleton() {
}
public Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
上面的代码就已经涵盖了单例模式最重要的三个要素:
- 将构造方法私有化(保证外部不能直接构造)。
- 有一个静态属性指向实例
- 提供一个公有的静态方法向外面提供这个实例。
上面看似完美的代码,其实有很大问题:在单线程中看似是没有什么问题的,但如果放在多线程的环境中就会有问题了。假如有两个线程同时访问getInstance方法,其中一个线程刚刚进入if (singleton == null){}里面,这个时候另一个线程恰好也访问这个方法,并且完成创建了一个实例,第一个线程继续运行的话就会再创建一个实例,单例失败。
基本方法的改进
既然了解了问题,那么我们如何才能防止两个线程同时实例化方法呢?有经验的同学或许就会立刻想到了通过synchronized
关键字进行加锁。
public class Singleton {
private static Singleton singleton;
private Singleton (){
}
public synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
这种写法能够保证多线程情况下所有线程只获取同一个实例,但该代码还是有问题:每次调用getInstance方法时都需要进行同步,如果当某个线程正在访问该方法的时候其他线程就只能在锁池中等待该线程释放锁。造成不必要的开销。而我们其实只有很少情况需要同步。
再次稍加改进一下:
/** 双重检查模式 */
public class Singleton {
private static Singleton singleton;
private Singleton (){
}
public static Singleton getInstance() {
if (instance== null) {
synchronized (Singleton.class) {
if (instance== null) {
instance= new Singleton();
}
}
}
return singleton;
}
}
这种写法在只有在instance== null时才会同步。
思考一下,这里为什么要做第二次if (instance== null)判断?
第一个判断是没有同步的,所以两个线程会同时进入第一个if (instance== null),第一个线程 拿到锁-实例化-释放锁 之后,第二个线程也进来了,如果此时不做if判断,第二个线程仍然会去实例化该对象,所以这里必须加if判断。
看似已经很完美了,那么我们有什么其他办法不用加锁的方式也能避免多线程的问题呢?ok,当然有的,我们可以使用饿汉式的单例:
/** 饿汉式 */
public class Singleton {
public Singleton singleton = new Singleton();
private Singleton() {
}
public Singleton getInstance() {
return singleton;
}
}
上面代码与懒汉式加载最大的区别在于这里的single在开始就实例化了,也就是不管我们是否使用它,都会将其加载到内存中去。这个在获取的时候就直接返回就行了。如果不在意内存的话最好使用这个方法。
双重检查模式(DCL模式)优点是资源利用率高,第一次执行getInstance时单例对象才被实例化,效率高。缺点是第一次加载时反应稍慢一些,它在高并发环境下也有一定的缺陷,虽然发生的概率很小。但是他还是在某些情况会出现失效的问题,也就是DCL失效。在《java并发编程实践》一书建议用静态内部类单例模式来替代DCL。
/** 静态内部类模式 */
public class Singleton {
private Singleton(){
}
public static Singleton getInstance(){
return SingletonHolder.sInstance;
}
private static class SingletonHolder {
private static final Singleton sInstance = new Singleton();
}
}
第一次加载Singleton类时并不会初始化sInstance,只有第一次调用getInstance方法时虚拟机加载SingletonHolder 并初始化sInstance ,这样不仅能确保线程安全也能保证Singleton类的唯一性,所以推荐使用静态内部类单例模式。
那么,还有别的方式了么?其实还可以使用枚举模式
/** 枚举式 */
public enum Singleton {
INSTANCE;
public void doSomeThing() {
}
}
枚举单例的优点就是简单,但是大部分应用开发很少用枚举,可读性并不是很高,不建议用。
上述讲的几种单例模式实现中,有一种情况下他们会重新创建对象,那就是反序列化,将一个单例实例对象写到磁盘再读回来,从而获得了一个实例。反序列化操作提供了readResolve方法,这个方法可以让开发人员控制对象的反序列化。在上述的几个方法示例中如果要杜绝单例对象被反序列化是重新生成对象,就必须加入如下方法:
private Object readResolve() throws ObjectStreamException{
return singleton;
}
总结:这里一共讲了五种(双重检查模式,饿汉式,静态内部类模式,枚举模式)单例的写法,至于选择用哪种形式的单例模式,取决于你的项目本身,是否有复杂的并发环境,或者需要控制单例对象的资源消耗。