设计模式-3-单例模式

单例的四大原则

  1. 私有构造方法
  2. 以静态方法或者枚举返回实例
  3. 确保实例只有一个,尤其是多线程环境下
  4. 确保使用反射和反序列化时不会构造新的对象

单例模式的不同分类

饿汉式、懒汉式、静态内部类方式、枚举方式

饿汉式写法

public class SingleTon {
    private final static SingleTon instance = new SingleTon();

    private SingleTon() {
    }

    public static SingleTon getInstance() {
        return instance;
    }
}

加不加final没有什么区别,都是线程安全的。
虚拟机会保证一个类的<clinit>()方法在多线程下被正确的加锁、同步,如果多个线程同时去初始化一个类,只有一个线程去执行这个类的<clinit>()方法,其他线程都需要堵塞等待,直到活动线程执行<clinit>()方法完毕,所以private static SingleTon instance = new SingleTon();这句是线程安全的,会保证instance被第一个触发线程完全初始化的后才被后续线程可见

懒汉式

public class SingleTon {
    private static volatile SingleTon instance;

    private SingleTon() {
    }

    // 1:线程不安全 即使对instance增加volatile修饰也没有用
    public static SingleTon getUnsafeInstance() {
        if (instance == null) {
            instance = new SingleTon();
        }
        return instance;
    }

    // 2:同步方法——每次获取实例都会有synchronized消耗
    // 并且不需要增加volatile修饰,因为synchronized会保证方法体作为一个整体,所以方法体结束时instance肯定是被完整初始化后的对象
    public static synchronized SingleTon getSafeInstance() {
    	if(instance == null){
    		instance = new SingleTon();
    	}
        return instance;
    }

    // 3:双层检查
    // instance变量需要被volatile修饰
    public static SingleTon getSafeInstanceDCL() {
        if (instance == null) {
            synchronized (SingleTon.class) {
                if (instance == null) {
                    instance = new SingleTon();
                }
            }
        }
        return instance;
    }
}

下面对每个方法说明

  • 方法1的线程不安全很明显,假如线程A线程B存在并发时,因为无锁保障,线程A、B同时判断instance==null条件满足,进而都对instance进行初始化得到不同的对象
  • 方法2是线程安全的,并且instance不需要被volatile修饰,不足之处在于每次获取实例都会有synchronized消耗,影响性能
  • 方法3多线程并发下可能会得到一个不完整的instance,为什么这么说呢,这里需要细分instance = new SingleTon();执行过程,这行代码虚拟机拆分为下面三行代码执行
//1:分配对象所需的内存空间
memory = allocate();
//2:初始化对象
ctorInstance(memory);
//3:将对象赋值给instance
instance = memory;

代码2和3可能会被重排序,导致对象还没有被初始化就赋值给instance,假设两个线程A和B,A先获取同步锁进入方法,执行到重排序后的3instance=memory此时2还没有执行,但是instance已经有指定的内存空间,所以不为null,线程B执行到外层第一个判断if(instance == null)时,不成立直接返回一个不完整的未被初始化的instance,这个对象显然不是我们期望的
解决办法:对instance做volatile修饰,volatile会禁止指令重排序,所以第3行代码一定是在第2行代码之后执行,并且volatile会强制线程从主内存中取值,所以B线程拿到的值要么为null要么是初始化后的结果;这里的volatile是为了保证初始化对象的有序性

静态内部类

public class SingleTon {
    private SingleTon() {
    }
    private static class SingleTonInner {
        private static SingleTon instance = new SingleTon();
    }
    public static SingleTon getInstance() {
        return SingleTonInner.instance;
    }
}

静态内部类方式的实现的核心原理在于外部类加载时不会立即加载内部类,内部类不被加载则不去初始化instance,不占内存。当调用getInstance()方法时SingleTonInner才会加载到SingleTon的运行时常量池里,把符号引用替换为直接引用,这是静态对象的instance被真正的创建,实现了懒加载保证了线程安全和单例的唯一性

枚举

public enum SingleTon {
    INSTANCE;
    public SingleTon getInstance() {
        return INSTANCE;
    }
}

枚举方式使用很简单,并且可以避免反射和序列化对单例的破坏,后面会对其他几种方式实现存在的问题以及使用枚举的好处做说明

单例模式对比和存在问题的解决方法

饿汉式、懒汉式、静态内部类方式都存在反射和序列化对单例破坏的问题

  • 反射对单例的破坏
  1. 核心代码
//getDeclaredConstructor可以获取类(默认无参)的私有构造方法
Constructor<SingleTon> constructor = SingleTon.class.getDeclaredConstructor();
//获取私有方法的调用执行权限
constructor.setAccessible(true);
//构建一个全新的实例
SingleTon instance3 = constructor.newInstance();
  1. 解决办法:在私有构造方法中判断instance是否为null,如果为null抛异常即可
private SingleTon() {
    if (instance != null) {
        throw new RuntimeException();
    }
}
  • 序列化对单例的破坏
  1. 代码如下
public class SingleSerializableHazardTest {
    public static void main(String args[]) throws FileNotFoundException, IOException, ClassNotFoundException {
        SerSingle instance1 = SerSingle.getInstance();
        instance1.setContext("序列化测试");
        SerSingle instance2 = SerSingle.getInstance();
        System.out.println(instance2.getContext() + " & instance1 == instance2:" + (instance1 == instance2));
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerSingle.obj"));
        oos.writeObject(instance1);
        oos.flush();
        oos.close();
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("SerSingle.obj"));
        SerSingle instance3 = (SerSingle) ois.readObject();
        ois.close();
        System.out.println(instance3.getContext() + " & instance1 == instance3:" + (instance1 == instance3));

    }
}

class SerSingle implements Serializable {
    private String           context;
    private static SerSingle instance = new SerSingle();
    private SerSingle() {
    }
    public static SerSingle getInstance() {
        return instance;
    }
    public void setContext(String context) {
        this.context = context;
    }
    public String getContext() {
        return context;
    }
}
// 输出结果
// 序列化测试&instance1 == instance2:true
// 序列化测试&instance1 == instance3:false

反序列化的对象和原对象不相等,任何一个readObject方法,不管是显式的还是默认的,都会返回一个新建的实例,这个新建的实例不同于该类初始化的实例
2. 解决办法:在SerSingle类中重写readResolve方法,这个方法时基于回调的,反序列化时,如果定义了readResolve()则直接返回此方法指定的对象,而不需要创建新的对象,代码如下:

private Object readResolve() throws ObjectStreamException {
    return instance;
}
  1. 避免反射对枚举单例规则的破坏
    • 枚举类型的单例没有默认的无参构造函数,只有一个从Enum继承的Enum(String,int)的构造方法
    • Constructor.newInstance()方法在实例化对象时,判断了类是否被Enum修饰,如果是则抛出异常
  2. 避免序列化对单例规则的破坏:枚举类型在序列化时writeObject对象Enum做了特殊处理,仅仅将枚举对象的name属性输出到结果中,反序列化时通过java.lang.Enum的valueOf方法根据名字查找枚举对象,并且Enum禁用了readObject和readObjectNoData方法

参考

深入理解单例模式:静态内部类单例原理
双重检查问题和解决方案
为什么要用枚举实现单例模式(避免反射、序列化问题)
单例模式的漏洞,通过反射和序列化、反序列化来破解单例,以及如何避免这些漏洞
枚举单例如何保证反序列化不创建新的实例

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值