单例模式(升级版)

设计模式

单例

​ 一个类只允许有一个对象,建立一个全局的访问点,提供出去供大家使用。

​ 好处:可以让两个对象在完全没有关系的前提下,实现值的传递,降低了耦合性,提高了内聚性。

懒汉
public class Singleton {
    // 将自身实例化对象设置为一个属性,并用static修饰
    private static Singleton instance;
    // 构造方法私有化
    private Singleton() {}
    // 静态方法返回该实例
    public static Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
懒汉2
public class Singleton {
    // 将自身实例化对象设置为一个属性,并用static修饰
    private static Singleton instance;
    // 构造方法私有化
    private Singleton() {}
	// 静态方法返回该实例,加synchronized关键字实现同步
    public static synchronized Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
线程安全的懒汉升级版(常用方式)
public class Singleton {
    private static volatile Singleton instance;
    
    private Singleton() {}
	
    public static Singleton getInstance() {
        if(instance == null) {
            synchronized(Singleton.class){
                if(instance==null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
双校验原理

这里会发现一个不同寻常的地方:private static volatile Singleton instance;

  • 为什么有volatile。

    这是因为虚拟机在实现instance = new Singleton();的时候,总共做了一下三个步骤:

    1. 给instance分配内存空间
    2. 调用Singleton构造函数来初始化成员变量
    3. 将instance指向分配的内存空间

    ​ 然而这三步中的后两步因为JVM的自动优化执行顺序可能是1-2-3也可能是1-3-2。这在单线程下是没有问题的。但是在多线程下,如果第一个线程执行的是1-3-2。当执行到3的时候线程被第二个线程抢走了。那么因为instance已经指向一个内存空间了,非null成立,就直接返回instance了。这样调用的时候就出现异常了(未初始化)。而用了volatile可以避免这种情况。volatile不能保证禁止指令重排,但是可以确保变量赋值过程全部结束才会被正常调用。解决了不满足原子性所带来的问题。

饿汉
public class Singleton {
    // 将自身实例化对象设置为一个属性,并用static、final修饰
    private static final Singleton instance = new Singleton();
    // 构造方法私有化
    private Singleton() {}
    // 静态方法返回该实例
    public static Singleton getInstance() {
        return instance;
    }
}
优质解法——静态内部类《剑指offer》

知识点:内部类是延时加载的,也就是说只会在第一次使用时加载(调用getInstance()的时候加载)。

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

似乎静态内部类已经是最优解了。延迟加载达到了懒汉的空间利用率,还是线程安全的。但是,我们依旧可以使其生成两个实例对象。比如反射攻击或反序列化攻击。

攻击测试
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    //反射攻击
    //1.直接通过getInstance()获取对象
    Singleton singleton=Singleton.getInstance();
    //2.通过反射获取单例类的构造器
    Constructor<Singleton> constructor=Singleton.class.getDeclaredConstructor();
    //3.设置私有的构造器可使用
    constructor.setAccessible(true);
    //4.通过构造器new一个实例对象
    Singleton newSingleton=constructor.newInstance();
    //5.判断两次的实例对象是否相等
    System.out.println(singleton==newSingleton);//false
    
    //序列化攻击
    //1.直接获取实例对象
    Singleton singleton1 = Singleton.getInstance();
    //2.序列化写出对象
    ObjectOutputStream oop 
        = new ObjectOutputStream(new FileOutputStream("test.txt"));
    oop.writeObject(Singleton.getInstance());
    //3.反序列化读回对象
    ObjectInputStream objectInputStream 
        = new ObjectInputStream(new FileInputStream("test.txt"));
    Singleton object = (Singleton) objectInputStream.readObject();
    //4.判断两次的实例对象是否相等
    System.out.println(object == singleton1);//false
    oop.close();
    objectInputStream.close();
}

以上两种攻击,虽然能破坏静态内部类的单例,但是平时也很少见。所以平时这个方法应该是极其优秀的方法。

最优解——枚举单例《Effective Java》
public enum EnumSingle{
    INSTANCE;
    public void doSomething(){
        System.out.println("do something here");
    }
    //测试
    public static void main(String[] args) {
        //获取对象实例并使用
        EnumSingle.INSTANCE.doSomething();
    }
}

enum保证这个INSTANCE是final static(可以反编译查看一下),同时可以知道虚拟机会保证一个类的*()* 方法在多线程环境中被正确的加锁、同步。所以,枚举实现是在实例化时是线程安全。

而序列化的时候,枚举只会把这个INSTANCE输出,反序列化的时候再通过这个INSTANCE查找对应的枚举类,所以保证了自由序列化。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值