一文搞定单例模式

1 基本概念

1.1 什么是单例模式

单例模式是一种创建型设计模式,让你能够保证一个类只有一个实例,并提供一个访问该实例的全局方法。

创建型模式:这类模式提供创建对象的机制,能够提升已有代码的灵活性和可复用性。

1.2 为什么要使用单例模式

  • 解决资源冲突问题
    对于一些全局资源,例如我们有个程序是使用打印机(项目里只有一个),会有多个请求要使用打印机,但是不能重复创建打印机资源。
  • 全局唯一类
    有些数据在系统中只应该保留一份。 比如配置信息类,系统的配置文件应该只有一份,加载到内存之后以对象的形式存在,保护该实例不被其他代码覆盖。
    有些经常被各个地方代码调用的类,例如数据库连接类通过 getInstance(获取实例)方法进行定义以让客户端在程序各处都能访问相同的数据库连接实例。

常见应用场景:

  • logging
  • drivers objects
  • caching
  • thread pool
  • java.lang.Runtime / java.awt.Desktop
  • Spring中Bean的默认生命周期

1.3 基本解决方案

单例模式的实现都包含以下两个基本的步骤:

  • 将构造函数设为私有,防止其他对象使用单例类的new运算符,即不能在单例类外实例化,只能在单例类内实例化
  • 在本类中创建本类private static的实例,必须自己创建该唯一实例
  • 提供一个访问该实例的全局函数,该方法会调用单例类私有构造函数来创建对象,并将其保存在一个静态成员变量中。此后所有对于该函数的调用都将返回这一缓存对象。

如果你的代码能够访问单例类,那它就能调用单例类的静态方法。 无论何时调用该方法, 它总是会返回相同的对象。

结构模式图如下所示:
在这里插入图片描述
单例需要考虑以下几个问题

  • 多线程创建时是否有线程安全问题
  • 支持延迟加载
  • getInstance()性能高吗

1.4 优缺点

优点:

  • 可以保证一个类只有一个实例
  • 获得了一个指向该实例的全局访问节点,可以优化共享资源的访问
  • 仅在首次请求单例对象时对其进行初始化,避免对象的频繁创建和销毁,可以提高性能

缺点:

  • 单例模式可能掩盖不良设计,比如程序各组件之间相互了解过多等。
  • 单例的客户端代码单元测试可能会比较困难,因为许多测试框架以基于继承的方式创建模拟对象。由于单例类的构造函数是私有的,而且绝大部分语言无法重写静态方法,所以你需要想出仔细考虑模拟单例的方法。要么干脆不编写测试代码,或者不使用单例模式。
  • 单例模式在多线程环境下需要进行特殊处理,避免多个线程多次创建单例对象。

2 典型实现方式

2.1 饿汉式-静态变量

饿汉式的特点就是直接创建,不管是不是现在需要,先创建了再说。
类加载时,直接在静态变量处实例化。

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

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

优点

  • getInstance()性能好,线程安全,实现简单
  • 由于使用了static关键字,保证了在引用这个变量时,关于这个变量的所以写入操作都完成,所以保证了JVM层面的线程安全

缺点

  • 如果一个类占用内存资源比较多,我们在初始化的时就加载了这个类,但是我们长时间没有使用这个类,导致了内存空间的浪费

2.2 饿汉式-静态代码块

这个与2.1的区别就是在一个静态代码块里实例化,但是把new放在static代码块有别的好处,那就是可以做一些别的操作,如初始化一些变量,从配置文件读一些数据等。

public class StaticBlockSingleton {
    
    

    private static StaticBlockSingleton instance;
    
    private StaticBlockSingleton(){
    
    }
    
    static{
    
    
        try{
    
    
            // do something else
            instance = new StaticBlockSingleton();
        }catch(Exception e){
    
    
            throw new RuntimeException("Exception occured in creating singleton instance");
        }
    }
    
    public static StaticBlockSingleton getInstance(){
    
    
        return instance;
    }
}

2.3 懒汉式-单线程

需要的时候再实例化

public class LazyInitializedSingleton {
    
    

    private static LazyInitializedSingleton instance;
    
    private LazyInitializedSingleton(){
    
    }
    
    public static LazyInitializedSingleton getInstance(){
    
    
        if(instance == null){
    
    
            instance = new LazyInitializedSingleton();
        }
        return instance;
    }
}

上述实现只适用于单线程场景,多线程情况下可能发生线程安全问题,导致创建不同实例的情况发生。如果是多线程同时调用getInstance(),会有并发问题啊,多个线程可能同时拿到instance == null的判断,这样就会重复实例化,单例就不是单例。解决见下面

2.4 懒汉式-synchronized

通过使用synchronized修饰getInstance()方法保证同步访问该方法,变成了严格的串行制,但是性能会比较差,即使是几个第一次请求单例类的线程。

public class ThreadSafeSingleton {
    
    

    private static ThreadSafeSingleton instance;
    
    private ThreadSafeSingleton(){
    
    }
    
    //synchronized修饰函数
    public static synchronized ThreadSafeSingleton getInstance(){
    
    
        if(instance == null){
    
    
            instance = new ThreadSafeSingleton();
        }
        return instance;
    }
}

2.5 懒汉式-Double check-volatile

该方式通过缩小同步范围提高访问性能,同步代码块控制并发创建实例。采用双重检验(内外两个判空)

public class ThreadSafeSingleton {
    
    

    private static volatile ThreadSafeSingleton instance;
    
    private ThreadSafeSingleton(){
    
    }
    
    public static ThreadSafeSingleton getInstanceUsingDoubleLocking(){
    
    
    	if(instance == null){
    
    
        	synchronized (ThreadSafeSingleton.class) {
    
    
            	if(instance == null){
    
    
                	instance = new ThreadSafeSingleton();
            	}
        	}
    	}
    	return instance;
	}
}

内层的判空的作用:

  • 当两个线程同时执行第一个判空时,都满足的情况下,都会进来,然后去争锁,假设线程1拿到了锁,执行同步代码块的内容,创建了实例并返回,释放锁,然后线程2获得锁,执行同步代码块内的代码,因为此时线程1已经创建了,所以线程2虽然拿到锁了,如果内部不加判空的话,线程2会再new一次,导致两个线程获得的不是同一个实例。线程安全的控制其实是内部判空在起作用。

可以只加内层判空是ok的

外层的判空的作用;

  • 内层判空已经可以满足线程安全了,加外层判空的目的是为了提高效率。
  • 因为可能存在这样的情况:如果不加外层判空,线程1拿到锁后执行同步代码块,在new之后,还没有释放锁的时候,线程2过来了,它在等待锁(此时线程1已经创建了实例,只不过还没释放锁,线程2就来了),然后线程1释放锁后,线程2拿到锁,进入同步代码块中,判空不成立,直接返回实例。
  • 这种情况线程2是不是不用去等待锁了?因为线程1已经创建了实例,只不过还没释放锁。
  • 所以在外层又加了一个判空就是为了防止这种情况,线程2过来后先判空,不为空就不用去等待锁了,这样提高了效率。

volatile作用:

  • 在多线程的情况下,双重检查锁模式可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作,因为new那行代码并不是一个原子指令,会被分割成多个指令。

实例化对象实际上可以分解成以下4个步骤:
1. 为对象分配内存空间
2. 初始化默认值(区别于构造器方法的初始化)
3. 执行构造器方法
将对象指向刚分配的内存空间
编译器或处理器为了性能的原因,可能会将第3步和第4步进行重排序:
1. 为对象分配内存空间
2. 初始化默认值
3. 将对象指向刚分配的内存空间
4. 执行构造器方法
线程可能获得一个初始化未完成的对象

2.6 内部类

该方式是线程安全的,适用于多线程,利用了java内部类的特性:静态内部类不会自动随着外部类的加载和初始化而初始化,内部类是要单独加载和初始化的。此方式单例对象是在内部类加载和初始化时才创建的,因此它是线程安全的,且实现了延迟初始化。

public class BillPughSingleton {
    
    

    private BillPughSingleton(){
    
    }
    
    private static class SingletonHelper{
    
    
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }
    
    public static BillPughSingleton getInstance(){
    
    
        return SingletonHelper.INSTANCE;
    }
}

2.7 枚举

枚举是最简洁的,不需要考虑构造方法私有化。值得注意的是枚举类不允许被继承,因为枚举类编译后默认为final class,可防止被子类修改。常量类可被继承修改、增加字段等,容易导致父类的不兼容。枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。
优点:getInstance()访问性能高,线程安全
缺点:非延迟初始化

public enum EnumSingleton {
    
    

    INSTANCE;
    
    public static void doSomething(){
    
    
        //do something
    }
}

public class EnumSingletonEnumTest {
    
    
    public static void main(String[] args) {
    
    
        EnumSingletonEnum instance = EnumSingletonEnum.INSTANCE;
        System.out.println(instance);
        instance.doSomething();
    }
}

2.8 小结

方法 优点 缺点
饿汉式 - 静态变量 线程安全,访问性能高 不能延迟初始化
饿汉式 - 静态代码块 线程安全,访问性能高,支持额外操作 不能延迟初始化
懒汉式 - 单线程 访问性能高,延迟初始化 非线程安全
懒汉式 + synchronized 线程安全,延迟初始化 性能不高
懒汉式 + Double check + volatile 线程安全,延迟初始化, 访问性能高 -
内部类 线程安全,延迟初始化,访问性能高 -
枚举 线程安全,访问性能高,安全 不能延迟初始化

3 破坏单例模式的方法和防范措施

除枚举外的其他单例模式实现方式存在的两个问题,也正是这两个问题,导致了单例模式若不采取措施,会有被破坏的可能。

3.1 通过反射破坏单例模式

反射是通过强行调用私有构造方法生成新的对象。

import java.lang.reflect.Constructor;

public class ReflectionSingletonTest {
    
    

    public static void main(String[] args) {
    
    
        EagerInitializedSingleton instanceOne = EagerInitializedSingleton.getInstance();
        EagerInitializedSingleton instanceTwo = null;
        try {
    
    
            Constructor[] constructors = EagerInitializedSingleton.class.getDeclaredConstructors();
            for (Constructor constructor : constructors) {
    
    
                //Below code will destroy the singleton pattern
                constructor.setAccessible(true);
                instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
                break;
            }
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
        System.out.println(instanceOne.hashCode());
        System.out.println(instanceTwo.hashCode());
    }
}

防范方法

如果我们想要阻止单例破坏,可以在构造方法中进行判断,若已有实例,则阻止生成新的实例。

private Singleton(){
    
    
    if (instance != null){
    
    
        throw new RuntimeException("实例已经存在,请通过 getInstance()方法获取");
    }
}

3.2 序列化和单例模式

有时在分布式系统中,我们需要在单例类中实现可序列化接口,这样我们就可以在文件系统中存储它的状态,并在以后的时间点检索它。以下是一个实现可序列化接口的单例类demo:

import java.io.Serializable;

public class SerializedSingleton implements Serializable{
    
    

    private static final long serialVersionUID = -3423566758394737115L;

    private SerializedSingleton(){
    
    }
    
    private static class SingletonHelper{
    
    
        private static final SerializedSingleton instance = new SerializedSingleton();
    }
    
    public static SerializedSingleton getInstance(){
    
    
        return SingletonHelper.instance;
    }   
}

序列化单例类的问题是,每当反序列化它时,它都会创建该类的新实例。示例如下:

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;

public class SingletonSerializedTest {
    
    

    public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
    
    
        SerializedSingleton instanceOne = SerializedSingleton.getInstance();
        ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
                "filename.ser"));
        out.writeObject(instanceOne);
        out.close();
        
        //deserailize from file to object
        ObjectInput in = new ObjectInputStream(new FileInputStream(
                "filename.ser"));
        SerializedSingleton instanceTwo = (SerializedSingleton) in.readObject();
        in.close();
        
        System.out.println("instanceOne hashCode="+instanceOne.hashCode());
        System.out.println("instanceTwo hashCode="+instanceTwo.hashCode());
        
    }
}

输出如下,可以看到hashcode不一样,成功破坏了单例类,new出了一个新的单例对象。

instanceOne hashCode=2011117821
instanceTwo hashCode=109647522

防范方法

  1. 不实现序列化接口
  2. 如果必须实现序列化接口,可以重写反序列化方法readResolve(),反序列化时直接返回相关单例对象。
protected Object readResolve() {
    
    
    return getInstance();
}

3.3 cloneable接口的破坏

和可序列化接口有些类似,当需要实现单例的类允许clone()时,如果处理不当,也会导致程序中出现不止一个实例。

public class Singleton implements Cloneable{
    
    
    private static volatile Singleton mInstance;
    private Singleton(){
    
    
    }
    public static Singleton getInstance(){
    
    
        if(mInstance == null){
    
    
            synchronized (Singleton.class) {
    
    
                if(mInstance == null){
    
    
                    mInstance = new Singleton();
                }
            }
        }
        return mInstance;
    }
    @Override
    protected Object clone() throws CloneNotSupportedException {
    
    
        // TODO Auto-generated method stub
        return super.clone();
    }
}

public class SingletonDemo {
    
    

    public static void main(String[] args){
    
    
        try {
    
    
            Singleton singleton = Singleton.getInstance();
            Singleton cloneSingleton;
            cloneSingleton = (Singleton) Singleton.getInstance().clone();
            System.out.println(cloneSingleton == singleton);
        } catch (CloneNotSupportedException e) {
    
    
            e.printStackTrace();
        }
    }
}

结果是false

防范方法

重写clone()方法,调clone()时直接返回已经实例的对象。

protected Object clone() throws CloneNotSupportedException {
    
    
        return mInstance;
}

猜你喜欢

转载自blog.csdn.net/qq_32505207/article/details/109328222