单例模式的4种写法

单例模式是开发过程中常用的模式之一,首先了解下单例模式的四大原则:

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

饿汉模式

饿汉模式在类被初始化时就创建对象,以空间换时间,故不存在线程安全问题。

public class SingleTon{
    // 创建对象
   	private static SingleTon INSTANCE = new SingleTon();
    // 构造方法私有
 	private SingleTon(){}
    // 返回实例
	public static SingleTon getInstance(){ return INSTANCE; }
}

饱汉模式

饱汉是变种最多的单例模式。我们从饱汉出发,通过其变种逐渐了解实现单例模式时需要关注的问题。

基础饱汉

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

饱汉模式的核心就是懒加载。好处是更启动速度快、节省资源,一直到实例被第一次访问,才需要初始化单例;缺点是线程不安全,if语句存在竞态条件。

DCL(Double Check Lock)

上面说到基础饱汉模式的缺点是线程不安全,在多线程环境下无法保证实例唯一,因此只需要在关键语句上加锁即可解决问题:

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

第一种解决方式简单粗暴,直接在获取实例的方法上加锁,保证了多线程情况下实例唯一,但是synchronized操作是很耗时的,导致的结果就是并发性能极差,因此可对其进行修改:

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

这就是双重检查锁模式(DCL,Double Check Lock),在这种模式下,基本达到了理想的效果(懒加载+线程安全);

事实上,DCL模式仍存在一些问题,第11行中singleton = new Singleton()并不是一个原子操作,它在jvm中分为3步执行:

  1. memory = allocate(); //在堆内存开辟内存空间;
  2. initInstance(memory); // 在堆内存中实例化SingleTon里面的各个参数;
  3. instance = memory; //把对象指向堆内存空间。

由于jvm指令重排的机制,第3步操作是可能被优化到第2步操作之前的,此时若有新的线程进入该方法,经if判断后(因第3步操作使该对象指向了一块内存空间,所以if判断结果为false)会直接返回singleton,即引用instance指向内存memory时,该内存还未被初始化;这就是著名的DCL失效问题,解决方法也很简单,使用volatile关键字修饰即可;

Holder模式

我们既希望利用饿汉模式中静态变量的方便和线程安全;又希望通过懒加载规避资源浪费。Holder模式满足了这两点要求:核心仍然是静态变量,足够方便和线程安全;通过静态的Holder类持有真正实例,间接实现了懒加载。

public class Singleton {
    
    private static class SingletonHolder {
    	private static final Singleton singleton = new Singleton();
    	private SingletonHolder() {
    	}
  	}
    
  	private Singleton() {}

	public static Singleton getInstance() {
    	return SingletonHolder.singleton;
  	}
}

静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。即当SingleTon第一次被加载时,并不需要去加载SingleTonHoler,只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE,第一次调用getInstance()方法会导致虚拟机加载SingleTonHoler类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。

虚拟机会保证一个类的在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的方法,其他线程都需要阻塞等待,直到活动线程执行初始化方法完毕。

枚举模式

public enum Singleton {
  SINGLETON;
}

枚举在java中与普通类一样,都能拥有字段与方法,而且枚举实例创建是线程安全的,在任何情况下,它都是一个单例;但是缺点是可读性极差。
通过反编译我们可以看到枚举类的本质:
在这里插入图片描述

本质上和饿汉模式相同,区别仅在于公有的静态成员变量。

反射与反序列化攻击

以上介绍的几种模式,除了枚举模式外,其他模式均无法防止反射攻击和反序列化攻击;

反射演示:

public static void main(String[] args) throws Exception{
    SingleTon singleTon = SingleTon.getInstance();

    Constructor<SingleTon> constructor = SingleTon.class.getDeclaredConstructor();
    constructor.setAccessible(true);
    SingleTon singleTon1 = constructor.newInstance();
    System.out.println(singleTon1 == singleTon);
    // 结果为false
}

解决办法:反射是通过构造方法构建实例的,那么只需要在构造方法中加入判空即可:
在这里插入图片描述

反序列化演示(需单例类实现Serializable接口):

public static void main(String[] args) throws Exception{
    SingleTon singleTon = SingleTon.getInstance
    FileOutputStream out = new FileOutputStream("SingleTon.txt");
    ObjectOutputStream oos = new ObjectOutputStream(out);
    oos.writeObject(singleTon);
    oos.close();
    out.close
    FileInputStream in = new FileInputStream("SingleTon.txt");
    ObjectInputStream obj = new ObjectInputStream(in);
    SingleTon singleTon1 = (SingleTon) obj.readObject();
    in.close();
    obj.close();
    System.out.println(singleTon == singleTon1);
    // 输出结果为false
}

解决办法:定义readResolve()方法,反序列化时,如果定义了readResolve()则直接返回此方法指定的对象。而不需要单独再创建新对象。
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值