单例模式 反射入侵 序列化入侵 饿汉式 懒汉式 双重校验锁 枚举 内部类

  • Post author:
  • Post category:其他




单例模式



单例模式定义

单例模式(singleton pattern)是一个比较简单的模式。即任何情况下,一个类在整个系统中都有且仅有一个实例。

单例模式通用类图:

在这里插入图片描述



单例模式的优点

  • 由于单例模式在内存中只有一个实例,

    减少内存开支

    ,特别是一个对象需要频繁地创建销毁时,而且创建或销毁时性能又无法优化,单例模式就非常明显了
  • 由于单例模式只生成一个实例,所以,

    减少系统的性能开销

    ,当一个对象产生需要比较多的资源时,如读取配置,产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。



饿汉式——(静态常量、静态代码块)



懒汉式——静态常量

通过三个步骤完成静态常量,饿汉式单例模式。

  1. 私有化类的构造器,放置通过new关键字创建对象。
  2. 通过静态常量,在类加载的时候直接new一个自身私有的实例,即为单例。
  3. 对外提供一个返回该单例的公共方法。

    至此,我们就完成了单例模式的实现。
class HungryStaticConstant {
    // 1. 私有化构造器。放置外部通过new创建对象。
    private HungryStaticConstant() {
    }

    // 2. 由本来自己创建一个实例,存放在静态常量中。
    private static final HungryStaticConstant singleton = new HungryStaticConstant();

    // 3. 提供一个公共静态方法,返回唯一的实例。
    public static HungryStaticConstant getSingleton() {
        return singleton;
    }
}

下面我们来测试一下该单例模式。

public class HungryStaticConstantTest {

    public static void main(String[] args) throws Exception {
        // 1. 正常情况下,单例模式是不会被打破
        HungryStaticConstant singleton1 = HungryStaticConstant.getSingleton();
        HungryStaticConstant singleton2 = HungryStaticConstant.getSingleton();
        // 注意singleton1 和 singleton2的hashCode值是一致的。表示单例模式是成功的。
        System.out.println(singleton1.hashCode());
        System.out.println(singleton2.hashCode());
    }
}

执行完成后可以看到打印的hash code值是一致。说明我们的单例模式是成功的。



入侵单例模式——反射

接下来我们通过反射的形式来获取单例的实例,看是否可以打破单例的模式。

public class HungryStaticConstantTest {

    public static void main(String[] args) throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, IOException, ClassNotFoundException {
        // 1. 正常情况下,单例模式是不会被打破
        HungryStaticConstant singleton1 = HungryStaticConstant.getSingleton();
        HungryStaticConstant singleton2 = HungryStaticConstant.getSingleton();
        // 注意singleton1 和 singleton2的hashCode值是一致的。表示单例模式是成功的。
        System.out.println(singleton1.hashCode());
        System.out.println(singleton2.hashCode());

        // 2. 通过反射的形式,打破单利模式。
        Class<HungryStaticConstant> HungryStaticConstantClass = HungryStaticConstant.class;
        Constructor<HungryStaticConstant> declaredConstructor =
                HungryStaticConstantClass.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        HungryStaticConstant singleton3 = declaredConstructor.newInstance();
        // 注意singleton3的hashCode值与1和2的是不同的,表明该单例模式已被打破。
        System.out.println(singleton3.hashCode());
    }
}

通过反射我们获取到了单例类的私有构造器,创建了singleton3对象。

打印结果可以看出singleton1和singleton2的hash值是相等的,但是singleton3是不相等的。

因此,通过反射的方式是可以打破单例模式的。

接下来我们通过在单例类的构造器中加入以下代码,防止单例模式被反射入侵。

再一次运行main方法,我们会发现当单例被反射入侵时,会抛出RuntimeException(“单例模式被入侵!”)异常。即完成了防止反射的入侵。

class HungryStaticConstant {
    // 1. 私有化构造器。放置外部通过new创建对象。
    private HungryStaticConstant() {
        synchronized (HungryStaticConstant.class){
            if (singleton == null){
                throw new RuntimeException("单例模式被入侵!");
            }
        }
    }
}



入侵单例模式——序列化

在java的有一个序列化的接口Serializable,如果我们让HungryStaticConstant implements Serializable实现这个接口。那么通过序列化的操作也是可以打破单例模式的。

让单例类实现Serializable接口后,我们在main方法中加入新的测试案例。

如所示:

public class HungryStaticConstantTest {

    public static void main(String[] args) throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, IOException, ClassNotFoundException {
        // 1. 正常情况下,单例模式是不会被打破
        HungryStaticConstant singleton1 = HungryStaticConstant.getSingleton();
        HungryStaticConstant singleton2 = HungryStaticConstant.getSingleton();
        // 注意singleton1 和 singleton2的hashCode值是一致的。表示单例模式是成功的。
        System.out.println(singleton1.hashCode());
        System.out.println(singleton2.hashCode());

        // 3. 当单例可以被序列化时,在反序列化时打破单例模式。
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(singleton1);

        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);

        HungryStaticConstant singleton4 = (HungryStaticConstant)ois.readObject();
        System.out.println(singleton4.hashCode());
    }
}

通过3部分所示的代码,我们先序列化单例的对象,然后再通过反序列的话方式创建singleton4。

运行结果可以发现singleton4的hash值与singleton1、singleton2是不一致的,单例模式被打破。

要防止可以序列化的单例类被入侵,在单例类中加入以下方法可以解决防止入侵。

加入此方法之后,在运行main方法测试。singleton1、singleton2、singleton3的hash值都是一样的。

class HungryStaticConstant {
    private Object readResolve() {
        return singleton;
    }
}    



懒汉式——静态代码块

处理通过静态常量完成饿汉式之外,用静态代码块也是一样。原理与静态常量类似,不做过多说明。

代码如下所示:

class HungryStaticBlock {
    // 1. 私有化构造器。放置外部通过new创建对象。
    private HungryStaticBlock() {
    }

    // 2. 由本来自己创建一个实例,存放在静态常量中。创建的过程在静态代码块中实现。
    private static HungryStaticBlock singleton = null;
    static {
        singleton = new HungryStaticBlock();
    }

    // 3. 提供一个公共静态方法,返回唯一的实例。
    public static HungryStaticBlock getSingleton() {
        return singleton;
    }
}



饿汉式的优缺点:


优点:

  • 写法简单,便于理解。通过在类加载时完成实例化。可以避免多线程的问题。


缺点:

  • 不能控制加载时机,不能达到懒加载的效果。如果始终未使用该类时,会造成内存浪费。还会加重服务器启动时间。



懒汉式



懒汉式——直接锁

由于饿汉式不能控制加载时机,我们在饿汉式的基础上再做修改。

class Lazy{
    private static Lazy singleton;

    // 私有化构造器。
    private Lazy(){
    }

    // 增加锁保证线程安全,避免在多线程的情况下创建多个实例,打破单利模式。
    public synchronized static Lazy getSingleTon(){
        if (singleton == null){
            singleton = new Lazy();
        }
        return singleton;
    }

}

从上面的代码中可以看出,我们将单例对象的初始化放到了getSingleTon方法中去。这样我们就将单例对象的加载推迟到了使用时。如果一直不使用不会创建此单例对象。

需要注意的时,为了防止多线程访问时,打破单例这里在方法上面加了synchronized的关键字,会影响性能。咋办呢,接着往下看…



懒汉式——双重检查锁

要解决懒加载锁的性能问题,引入了一个双重检查加锁的机制。

代码如下:

class SingletonDCL {
    private static volatile SingletonDCL singletonDCL;

    private SingletonDCL() {
    }

    public static SingletonDCL getSingletonDCL() {
        // 第一次校验
        if (singletonDCL == null) {
            // 锁。
            synchronized (SingletonDCL.class) {
                // 第二次校验
                if (singletonDCL == null) {
                    singletonDCL = new SingletonDCL();
                }
            }
        }
        return singletonDCL;
    }
}

我们分析一下getSingletonDCL执行流程,当有多个线程同时访问时。在懒汉式的案例中,不管有没有创建好单例对象都会等待其它线程占用的释放。性能很差。

这里我们先来第一次判断,看是否已经创建了单例对象,如果创建直接返回即可,不存在线程问题,也就避免了性能问题不用等待。

如果没有创建,那么我们进入加锁的代码块。这里需要注意,因为多线程的情况下,都可能进入第二次校验。

此时还没有创建单例对象,而进入的线程排队准备创建案例对象。所以我们此时进行第二次校验,那么只要有一个线程常见成功。由于这里是加锁的,后面的线程顺序执行的时候就不会再创建单例。既可以保证单例模式的安全。

因为这种模式我们只要需要等待一次单例的获取,所以基本上不存在获取单例的性能问题。



懒汉式——静态内部类

除了上述两种方式外,利用静态内部类的延迟加载特性。也可以完成懒汉式的单例模式。

代码如下:

class StaticInnerClass{

    // 构造器私有化。
    private StaticInnerClass(){
        System.out.println("外部内加载。");
    }

    // 提供一个静态内部类。
    private static class Instance{
        public Instance(){
            System.out.println("内部静态类加载。");
        }
        private static final StaticInnerClass instance = new StaticInnerClass();
    }

    // 提供静态的方法返回单例。
    public static StaticInnerClass getInstance(){
        System.out.println("调用getInstance发方法");
        return Instance.instance;
    }

}

通过私有的静态内部类来持有单例类的对象,完成对单例的延迟加载。

上述三种延迟加载模式,在实际的场景中,只有第一个是不能使用的存在多线程的安全隐患。

当然,上述三种懒汉模式也存在,之前说的反射和序列化的问题,这里就不在赘述。由于反射和序列化问题是程序员主动的编码问题,所以通常情况下,单例模式可以忽略这两个问题。



枚举单例模式

由于枚举的特殊性,使用枚举来完成单例模式可以很好地满足上述要求。

enum EnumSingleton{
    SINGLETON;

    private EnumSingleton(){
        System.out.println("枚举单例模式初始化");
    }

    public void doWork(){
        System.out.println("枚举单例模式");
    }
}



实战



JDK中的单例的运用

jdk中的Runtime类很明显就是一个懒汉式的加载模式。代码节选如下所示:


public class Runtime {
    private static Runtime currentRuntime = new Runtime();
    
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    private Runtime() {}
}



实际工作中

如我们需要在A系统中去对接B系统,而此时需要在A系统中完成一些访问B系统的初始化工作,且之后调用B系统过的功能都无需在重复初始化的工作时。我们可以使用单例模式。



版权声明:本文为escore原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。