设计模式之正确实现单例模式

单例模式是非常好理解的一个类,它的设计目标也非常简单:保持一个类仅有一个实例。

1.最普通的单例模式(不完美)

下面我们实现一个最简单的单例模式:

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

如上述代码所示,我们将Singleton的构造方法设为private的,这就保证了其他人不能通过正常方式new出Singleton的实例,该类中有一个该类对象的静态实例,以及一个getInstance()方法,人们只能通过这个getInstance()方法得到这个实例。

2.单线程情况下的单例模式(仅适用于单线程、延迟加载)

出于性能的考虑(由于instance是静态的,因此会在类加载的时候就被加载到方法区中),不少单例模式的实现会采用延迟加载(Lazy Loading)的方式,即在需要使用到该实例的时候才会被创建该实例,代码如下:

public class Singleton {
    public static Singleton instance;
    private Singleton(){}
    public static Singleton getInstance(){
        if(instance == null){          //操作1
            instance = new Singleton();//操作2
        }
        return instance;
    }
}

上述代码在单线程情况下可以正常工作,但是在多线程情况下,上述代码的if语句形成了一个check-then-act操作,它并不是一个原子操作;且代码并未采用任何同步措施,因此不是线程安全的。比如:在instance还是null的时候,线程T1调用getInstance()方法,检测到instance == null,还没有执行操作2,此时T1的时间片用完,线程T1被挂起,另一个线程T2被调度,T2执行操作1,发现instance == null为true,然后T2执行操作2 ,创建了instance的实例。此后T1再次被调度的时候,继续上次未执行完蛋代码,又创建了instance的一个实例。这就导致了多个实例的创建,从而违背了我们单例模式的初衷(保持一个类只有一个实例)。

3.加锁的单例模式(不完美、延迟加载)

不难想到,我们可以通过同步措施来解决上述问题(这里我们用synchronized关键字来实现),代码如下:

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

该方法实现的单例模式固然是线程安全的,但是这意味着getInstance()的任何执行线程都需要申请锁,增大了开销。

4.基于双重检查锁定的单例模式(有线程安全问题、延迟加载)

为了减少锁带来的开销,有的人提出了在执行临界区代码前先检查instance是否为null,若不为null,直接返回instance实例,否则才进入临界区。该方法被称为双重检查锁定(Double-checked Locking,DCL),代码如下:

public class Singleton {
    public static Singleton instance;
    private Singleton(){}
    public static Singleton getInstance() {
        if(instance == null) {   //操作1
            synchronized (Singleton.class)  {
                if(instance == null) {  //操作2 
                    instance = new Singleton();  //操作3
                }
            }
        }
        return instance;
    }
}

上述代码并不会出现在上一个例子(3)中的线程安全问题,但是线程安全问题不仅仅包括可见性,我们还需要考虑重排序带来的影响。

我们看一下操作3 的代码,为执行这一句代码,CPU会有这么几个动作:

objRef = allocate(Singleton.class);  //子操作1:分配对象存储空间
invokeConstructor(objRef);  //子操作2:执行该对象构造方法
instance = objRef;  //子操作3:将对象引用赋值给instance

根据多线程有关的知识,我们知道,临界区内的操作可以在临界区内被重排序,上述操作可能会被重排序为:子操作1->子操作3->子操作2,即对象还没有初始化,就将对象引用赋值给instance。那么我们通过getInstance()得到的就有可能为未初始化的对象。

5.基于双重检查锁定的单例模式(线程安全、延迟加载)

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

我们将instance用volatile修饰,就可保证上述代码的线程安全了,原因是volatile关键字会禁止volatile变量的写操作与之前的操作重排序(通过插入内存屏障实现,以后我会在多线程部分介绍)。

6.基于静态内部类的单例模式(推荐、延迟加载)

上例的代码可以保证线程安全,也可实现延迟加载,但缺点是代码不够优雅、不够简单,下面我们来实现一种优雅的写法:

public class Singleton {
    private static class InstanceHolder {
        final static Singleton INSTANCE = new Singleton();
    }
    private Singleton(){}
    public static Singleton getInstance() {
        return InstanceHolder.INSTANCE;
    }
}

内部类是延时加载的,使用的时候才加载,不使用就不加载,可以很好地实现单例模式。

类的静态变量的初次访问触发该类的初始化,类经初始化后其中的静态变量值会为其初始值而不是默认值。

上述代码中,getInstance()方法的调用会触发该类的内部静态类InstanceHolder的初始化,从而Singleton的唯一实例被创建。

虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步。如果多个线程同事去初始化一个类,那么只有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程的<clinit>()方法执行完毕。同一个类加载器下,一个类型只会被初始化一次,因此执行<clinit>()方法的那个线程退出<clinit>()方法后,其他线程被唤醒之后不会进入<clinit>()方法。因此保证了只会形成一个实例。

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------上述方法如果正常调用,是一个类加载器只有一个单例,但是可以通过反射、序列化和反序列化得到多个实例。

1. 反射调用构造方法初始化新的实例

FileIO fileIO = FileIO.class.newInstance();

对于通过反射调用构造方法的破坏方式我们可以通过在增加全局变量flag,在第一次初始化的时候就设置为true,第二次初始化的时候判断到flag为true就抛出异常。但这种办法也只能避免破坏,无法彻底阻止,因为他们可以反射flag来修改flag的值。

2. 序列化和反序列化产生新的实例

可参见:序列化之用序列化实现深复制

我们可以增加readResolve()方法来预防。在反序列化的时候会判断如果实现了serializable 或者 externalizable接口的类中又包含readResolve()方法的话,会直接调用readResolve()方法来获取实例。

import java.io.Serializable;

public class Singleton implements Serializable {
    private static class InstanceHolder {
        final static Singleton INSTANCE = new Singleton();
    }

    private Singleton() {
    }

    public static Singleton getInstance() {
        return InstanceHolder.INSTANCE;
    }

    private Object readResolve() {
        return InstanceHolder.INSTANCE;
    }
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值