单例模式(Singleton)

单例模式(Singleton)

在一些系统中,出于节省内存资源、保证数据内容一致性等考虑,对某一些类要求只能创建一个实例,这就是所谓的单例模式。

定义与特点

定义:一个类只有一个实例,且该类能自行创建这个实例。

特点:

  1. 单例类只有一个实例对象;
  2. 该单例对象必须由单例类自行创建;
  3. 单例类对外提供一个访问该单例的全局访问点。

优缺点

优点:

  1. 在内存中只有一个实例,减少了内存的开销;
  2. 避免了对资源的多重占用;
  3. 单例模式设置全局访问点,可以优化和共享资源的访问。

缺点:

  1. 单例模式没有抽象层,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现,违背了开闭原则。
  2. 单例模式功能代码通常写在一个类中,若设计不合理,很容易违背单一职责原则。

应用场景

  1. 需要频繁创建对象
  2. 某些类实例化占用资源过多或实例化耗时较长且经常使用
  3. 某些类需要频繁实例化,而创建的对象有频繁被销毁
  4. 频繁访问数据库或文件的对象
  5. 控制硬件级别的操作,或者从系统上来讲应该是单一控制逻辑的操作,若有多个实例会破坏系统正常运行
  6. 对象需要被广泛共享

结构与实现

结构

单例类:包含一个实例且能自行创建这个实例的类

访问类:使用实例的类

单例类Singleton
getInstance() : Singleton
访问类Client

实现

懒汉模式

加载时不生成单例,第一次调用getlnstance 方法时才去创建这个单例。

public class Singleton {
    private static Singleton singleton = null;
    //构造方法私有
    private Singletion(){}
    
    public static Singleton getlnstance() {
        //如果没有实例化,就实例化一个,然后返回
        if (singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

不过显然这段代码是线程不安全的,那么以下是线程安全的懒汉模式。

public class Singleton {
    private static volatile Singleton singleton = null;
    private Singletion(){}
    
    public static synchronized Singleton getlnstance() {
        //如果没有实例化,就实例化一个,然后返回
        if (singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

线程安全的懒汉模式使用volatile 和 synchronized保证了线程的安全,但是每次调用getlnstance方法都需要同步,会影响性能且消耗更多的资源。

饿汉模式

当类加载的时候就创建一个单例。

public class Singleton {
    private static Singleton singleton = new Singletion();
    private Singletion(){}
    
    public static Singleton getlnstance() {
        return singleton;
    }
}

饿汉模式不存在性能和线程安全问题(JVM加载类的时候是单线程的),但是因为加载时就创建,因此可能会产生很多无用的实例。

双重校验

双重校验和懒汉模式类似,也是一种延迟加载,但是解决了线程安全懒汉模式的效率问题。

public class Singleton {
    
    private static volatile Singleton singleton = null;
    private Singleton(){}

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

缩小了synchronized范围,避免大范围加锁造成的效率问题。
双重校验,第一重校验防止实例化后调用该方法代码加锁影响效率,第二重校验防止有线程A实例化时线程B等待锁造成线程B获取锁后再次实例化对象。
在new对象的时候,有三个步骤:分配内存空间,初始化对象,将内存地址赋值给变量;在这三个步骤中,步骤2和3有可能会在操作上进行重排序,在重排序的情况下,还没有初始化对象,先将内存地址赋值给了变量,当线程B进入时,发现变量不为null,就会直接返回这个实例,然而此时可能拿到的是还没有初始化完成的对象。所以双重锁需要volatile关键字防止重排序。

静态内部类
public class Singleton { 
    private Singleton(){
    }
      public static Singleton getInstance(){  
        return SingletonHolder.sInstance;  
    }  
    private static class SingletonHolder {  
        private static final Singleton sInstance = new Singleton();  
    }  
} 

Java静态内部类的加载的时候不会被加载,使用的时候才会进行加载。而使用到的时候类加载又是线程安全的,可以完美解决上述的问题。

单元素枚举类
public enum Singleton {
    INSTANCE;
    
    public void doSomething(){
        
    }
}

调用时:

Singleton.INSTANCE.doSomething();

枚举功能完整、使用简洁、无偿地提供了序列化机制、在面对复杂的序列化或者反射攻击时仍然可以绝对防止多次实例化。单元素的枚举类型被《Effective Java》作者认为是实现Singleton的最佳方法。

问题

类实例化的方式不只有new关键字,还有反射、反序列化和克隆。

除了单元素枚举类单例天生免疫反射、反序列化和克隆外,其他实现方法均会被反射、反序列化和克隆破坏单实例。

反射
破坏示例
Singleton singleton = Singleton.getInstance();
Class<?> singletonClass = singleton.getClass();
Constructor<?> singletonConstructor = singletonClass.getDeclaredConstructor();
singletonConstructor.setAccessible(true);//取消访问检查
Singleton newSingleton = (Singleton) singletonConstructor.newInstance();

任何类在反射机制面前都是透明的,通过反射机制可以获得类的各种属性,也可以获得类的构造器(就算是私有也没用),从而构造一个新的对象。

解决

我们可以在单例类中添加一个标志位来防止其多次实例化,以饿汉模式为例:

private static boolean flag = false;

private static Singleton singleton = new Singleton();

private Singleton(){
    if (!flag){
        flag = true;
    }else {
        throw new RuntimeException("不可多次实例化");
    }
}

public static Singleton getInstance(){
    return singleton;
}

但这其实没有什么意义,因为我们仍然可以利用反射机制修改标识位来实现多次实例化。

Singleton singleton = Singleton.getInstance();
Class<?> singletonClass = singleton.getClass();
//修改标志位
Field field = singletonClass.getDeclaredField("flag");
field.setAccessible(true);
field.set(singleton, false);
//再次实例化
Constructor<?> singletonConstructor = singletonClass.getDeclaredConstructor();
singletonConstructor.setAccessible(true);
Singleton newSingleton = (Singleton) singletonConstructor.newInstance();
反序列化
破坏示例
Singleton singleton = Singleton.getInstance();
//序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singletonTest"));
oos.writeObject(singleton);
oos.close();
//反序列化
File file = new File("singletonTest");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Singleton newSingleton = (Singleton) ois.readObject();
解决

作为解决,我们可以在单例类中添加一个新方法readResolve:

private Object readResolve() {
    return singleton;
}

这个readResolve方法只做了一个简单的事情,反序列化的时候,首先检查这个类有没有readResolve方法,若有,则返回readResolve指定的对象,若没有,才把反序列化的结果返回。

克隆
破坏示例
Singleton singleton = Singleton.getInstance();
Singleton newSingleton = (Singleton) singleton.clone();
解决

和反实例化的解决方法类似,在重写的clone方法中返回单例类自身:

@Override
public Object clone() throws CloneNotSupportedException {
    return singleton;
}
单元素枚举类单例为什么免疫这些问题
反射

看一下Constructor类源码:

public T newInstance(Object ... initargs){
    …………
    if ((clazz.getModifiers() & Modifier.ENUM) != 0)
    	throw new IllegalArgumentException("Cannot reflectively create enum objects");
    …………
}

底层源码直接禁止了通过反射机制来创建一个枚举对象。

反序列化

所有枚举类都是Enum类的子类,枚举中可以声明多个对象,每个枚举对象拥有两个唯一的属性:String name 和 int ordinal,name就是我们在声明枚举变量时的名字(比如INSTANCE),ordinal就是声明的顺序(比如若INSTANCE是第一个声明的,则为0)。

在java规范中对枚举类型的序列化和反序列化做了特殊规定,在序列化和反序列化期间,任何特定于类的writeObject,readObject,readObjectNoData,writeReplace和readResolve方法都会被忽略。 同样,任何serialPersistentFields或serialVersionUID字段声明也会被忽略,所有枚举类型的fixedserialVersionUID都是0L。(也就是说枚举类型序列化反序列化机制与其他类型的不一样)。序列化时,仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf()方法来根据名字查找枚举对象。

枚举在序列化和反序列化过程中不会创建新的对象,而是根据name拿到原有的对象,因此保证了单实例。

克隆

所有枚举类都是Enum类的子类,在Enum中clone方法被定义成protected final,且具体实现就只是抛出CloneNotSupportedException异常。

protected final Object clone() throws CloneNotSupportedException {
    throw new CloneNotSupportedException();
}

因此枚举类无法重写并实现clone方法,所以无法通过克隆创建新实例。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值