设计模式之单例模式

确保一个类在任何情况下都绝对只有一个实例,并提供一个全局的访问点,隐藏其所有的构造方法。
比如:皇帝
在这里插入图片描述

一、饿汉式单例

在单例首次加载的时候就创建实例

版本一:
public class HungrySingleton {
    private static final HungrySingleton hungrySingleton = new HungrySingleton();    //final 保证不会被其他方式所修改
    private HungrySingleton() {
    }        // 构造方法私有
    public HungrySingleton getHungrySingleton() {
        return hungrySingleton;
    }       //提供一个全局访问点
}

版本二:
public class HungryStaticSingleton {
    private static final HungryStaticSingleton hungryStaticSingleton; //final

    static {
        hungryStaticSingleton = new HungryStaticSingleton();
    }

    private HungryStaticSingleton() {
    } //私有的构造函数

    public HungryStaticSingleton getHungrySingleton() {
        return hungryStaticSingleton;
    } //全局的访问点

}

静态字段和静态块都会在类加载的时候被实例化,不管有没有用,都会先被实例化;
缺点:浪费内存空间

二、懒汉式单例

被外部类调用的时候才创建实例

版本一:
public class LazySingleton {
private static LazySingleton lazySingleton = null ;
private LazySingleton(){}        // 构造方法私有
public synchronized static LazySingleton getLazySingleton(){ //synchronized  保证线程安全
if(lazySingleton == null) { //保证只创建了一次
lazySingleton = new LazySingleton();}
return lazySingleton;
}       //全局访问点
}
存在问题:线程不安全,可能会存在同时有多个线程进入get方法,加了synchronized关键字,可解决此问题,如果有线程进入到该方法,则其他线程会进不去,也就是加上锁了,但是给静态方法加上锁了之后很有可能把整个类都锁上了,会很影响性能问题;

//版本二 双重检查锁
public class LazySingleton {
private volatitle static LazySingleton lazySingleton = null ; //volatitle 解决指令重排序的问题 
private LazySingleton(){}        // 构造方法私有
public static LazySingleton getLazySingleton(){ //synchronized  保证线程安全
if(lazySingleton == null) {
 // 保证所有线程都可以进入,不会因为锁方法而把类锁上,也可以过滤很多,提高性能
 synchronized(LazySingleton.class){ //开始锁住
	 if(lazySingleton == null) { //防止两个线程同城到达if,又出现线程不安全的问题,双重检查锁
			lazySingleton = new LazySingleton();}
 } } 
return lazySingleton;
}
}
volatitle:防止指令重排序
cpu执行时会转成成JVM指令执行,
1、分配内存给这个对象
2、初始化对象
3、将初始化好的对象和内存地址建立关联赋值;
4、用户初次访问

//版本三:静态内部类 - 性能最优的一种方案
public class LazyInnerClassSingleton {
private LazyInnerClassSingleton (){}        // 构造方法私有

public static LazyInnerClassSingleton getLazyInnerClassSingleton{ 
return LazyHolder.lazy;
}

private static class LazyHolder{
private static final LazyInnerClassSingleton  lazy = new LazyInnerClassSingleton();
}
}

静态内部类的写法没有用到synchronized关键字,所以性能最优;
利用了静态内部类加载的一个原理,大类加载的时候,首先会加载内部类,等调get的时候,内部类的逻辑就会执行,内部类的对象又是一个final 的大类对象,所以就会只会创建一个对象。利用了内部类的特性,内部类没有初始化就不能调,JVM底层的执行逻辑,完美的避免了线程问题。

扩展1:反射破坏单例
在单例模式的设计过程中,一定要将构造器设计为私有构造器,因为这样可以防止从外部构造对象。但是这样其实并不安全,因为通过反射我们可以获取类中的域、方法、构造器,可以修改他们的访问权限,这样可以通过反射构造对象。如下所示:通过全局访问点得到的和通过反射得到的,并不是同一个对象~
在这里插入图片描述

// 反射可能会攻击单例,强制访问私有构造函数
private LazyInnerClassSingleton (){
if(LazyHolder.lazy != null){
throw e;
}
} 

在这里插入图片描述
对构造函数进行改造,可以有效的防止被反射破坏,看似成功,暗藏bug。
饿汉式单例 --> 在单例首次加载的时候就创建实例
那么,正常的创建单例对象,和反射去强行创建,无论哪个顺序在前,反射创建的时候都会抛异常。
因为就算反射时如果类没有进行初始化,也会先执行类的初始化,然后再执行反射(java规范),
此时单例对象已经存在,反射就会抛出异常。

懒汉式单例 -->被外部类调用的时候才创建实例。
如果反射构造在前,正常的创建单例对象在后,那么反射的异常是抛不出来的,因为这个时候单例对象还不存在,
还是会创建出来两个不同的单例的。

扩展2:被序列化破坏
将一个实例序列化到一个文件中后,再反序列化得到一个实例,这两个实例是不同的,这就违反了单例原则
在这里插入图片描述

在反序列化时是通过反射newInstance了一个新的实例,我们可以通过下面方法防止这种序列化与反序列化的破坏:
private Object readResolve(){ return lazySingleton; } // 重写readResolve方法
// 可以重写readResolve方法,让他直接发挥常量对象值,它本身是重新调用构造函数创建一个对象。
private Object readResolve(){
return LazyHolder.lazy;
}

在这里插入图片描述
这个方法在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新new出来的对象。由于是反射调用,里面的Method对象已经规定了调用时的方法名为readResolve,在这里我们才加这样名称的一个方法。

三、注册式单例

将每个实例都缓存到统一的容器中去,使用唯一标识来获取实例
1、枚举式

public enum EnumSingleton {
    INSTANCE; //唯一标识
    private Object data;
    public Object getData() {
        return data;
    }
    public void setData(Object data) {
        this.data = data;
    }
    public static EnumSingleton getInstance() {
        return INSTANCE;
    }
}

不会被反射破坏:JDK为枚举不被序列化和反射破坏来保驾护航
在这里插入图片描述
原因:反射的时候,调用的newInstance方法中对构造的对象的class信息进行了判断,如果构造的是枚举类,则抛出异常,可见我们不能通过反射构造枚举类型的实例,这样就天然的使得枚举类实现的单例模式避免了反射攻击。
在这里插入图片描述

不会被序列化破坏:通过类名和枚举值可以确定唯一对象
在这里插入图片描述
反序列化的IO类中对枚举类对象的构造是用class类型与名称进行的,同一枚举类类型中名称相同的对象一定是同一个,在内存中只有一份,故反序列化出来的对象与原对象相同。这样就天然抵挡了序列化与反序列化的破坏。
在这里插入图片描述
枚举类反编译后,只有一个私有构造器,有一个静态初始化块,在类初始化阶段对枚举类的实例进行初始化,其实也就是饿汉式单例,再加上其在IO类中和反射类中的天然安全检查优势,自然可以防御反射攻击与序列化与反序列化破坏,所以算是单例模式的最佳实践。

2、容器式单例
将每个实例都缓存到统一的容器中去,使用唯一标识来获取实例

public class ContainerSingleton {
    private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();
    public static Object getBean(String className) {
        synchronized (ioc) { //防止线程安全
            if (!ioc.containsKey(className)) {
                Object obj = null;
                try {
                    obj = Class.forName(className).newInstance(); //反射
                    ioc.put(className, obj);
                } catch (Exception e) {
                }
                return obj;
            }
            return ioc.get(className);
        }
    }
}

四、总结

单例模式的优点:
在内存中只有一个实例,减少了内存开销;
可以避免对资源的多重占用;
可以设置全局访问点,严格控制访问;

缺点:
没有接口,扩展困难;
扩展单例对象,只能修改代码,没有其他方法,不符合开闭原则的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值