单例模式

1. 知识点

  • 单例模式定义
  • 单例模式类图
  • 单例模式示例
  • 单例模式应用

单例模式(Singleton Pattern),顾名思义,就是被单例的对象只能有一个实例存在。单例模式的实现方式是,一个类能返回对象的一个引用(永远是同一个)和一个获得该唯一实例的方法(必须是静态方法)。通过单例模式,我们可以保证系统中只有一个实例,从而在某些特定的场合下达到节约或者控制系统资源的目的。

2. 类图

图片描述信息
Image

3. 示例代码

1.饿汉模式

最常见、最简单的单例模式写法之一。顾名思义,“饿汉模式” 就是很 “饥渴”,所以一上来就需要给它新建一个实例。但这种方法有一个明显的缺点,那就是不管有没有调用过获得实例的方法(本例中为 getWife() ),每次都会新建一个实例,浪费空间。

// 饿汉模式
public class Wife {

    // 一开始就新建一个实例
    private static final Wife wife = new Wife();

    // 默认构造方法
    private Wife() {}

    // 获得实例的方法
    public static Wife getWife() {
        return wife;
    }
}
package com.gs.single;

/**
 * @author admin
 * @date 2021/8/28 1:51 下午
 */
public class Hungry {

    // 可能会浪费空间
    private byte[] data1 = new byte[1024 * 1024];
    private byte[] data2 = new byte[1024 * 1024];
    private byte[] data3 = new byte[1024 * 1024];
    private byte[] data4 = new byte[1024 * 1024];

    private Hungry () {

    }

    private final static Hungry HUNGRY = new Hungry();

    public static Hungry getInstance(){
        return HUNGRY;
    }
}

2.懒汉模式

最常见、最简单的单例模式之二,跟 “饿汉模式” 是 “好基友”。再次顾名思义,“懒汉模式” 就是它很懒,一开始不新建实例,只有当它需要使用的时候,会先判断实例是否为空,如果为空才会新建一个实例来使用。

// 懒汉模式
public class Wife {

    //一开始没有新建实例
    private static Wife wife;

    private Wife() { }

    // 需要时再新建
    public static Wife getWife() {
        if (wife == null) {
            wife = new Wife();
        }
        return wife;
    }
}

3.线程安全的懒汉模式

是不是感觉很简单?但是上面的懒汉模式却存在一个严重的问题。那就是如果有多个线程并行调用 getWife() 方法的时候,还是会创建多个实例,单例模式就失效了。

Bug 来了,改改改!

简单,我们在基本的懒汉模式上,把它设为线程同步(synchronized)就好了。synchronized 的作用就是保证在同一时刻最多只有一个线程运行,这样就避免了多线程带来的问题。关于 synchronized 关键字,你可以 点击这里 了解更多。

// 懒汉模式(线程安全)
public class Wife {
    private static Wife wife;

    private Wife() { }

    // 添加了 synchronized 关键字
    public static synchronized Wife getWife() {
        if (wife == null) {
            wife = new Wife();
        }
        return wife;
    }
}

4.双重检验锁(double check)

线程安全的懒汉模式解决了多线程的问题,看起来完美了。但是它的效率不高,每次调用获得实例的方法 getWife() 时都要进行同步,但是多数情况下并不需要同步操作(例如我的 wife 实例并不为空可以直接使用的时候,就不需要给 getWife() 加同步方法,直接返回 wife 实例就可以了)。所以只需要在第一次新建实例对象的时候,使用同步方法。

不怕,程序猿总是有办法的。于是,在前面的基础上,又有了 “双重检验锁” 的方法。

// 双重锁的 getWife() 方法
public static Wife getWife() {

    // 第一个检验锁,如果不为空直接返回实例对象,为空才进入下一步
    if (wife == null) {
        synchronized (Wife.class) {

            //第二个检验锁,因为可能有多个线程进入到 if 语句内
            if (wife == null) {
                wife = new Wife();
            }
        }
    }
    return wife ;
}
package com.gs.single;


/**
 * @author admin
 * @date 2021/8/28 1:54 下午
 */
public class Lazy {
    private Lazy() {
        System.out.println(Thread.currentThread().getName());
    }

    private static Lazy lazy;

    public static Lazy getInstance() {
        // 双重检测锁模式的懒汉式单例 DCL懒汉式
        if (lazy == null) {
            synchronized (Lazy.class){
                if (lazy == null) {
                    lazy = new Lazy();  // 不是原子性操作
                    /*
                    1. 分配内存空间
                    2. 执行构造方法,初始化对象
                    3. 把这个对象指向这个空间

                    可能发生指令重排现象
                    如132
                     */
                }
            }
        }
        return lazy;
    }

    // 多线程并发
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                Lazy.getInstance();
            }).start();
        }
    }
}

你以为这终于圆满了?NO...Too young, too naive! 主要问题在于 wife = new Wife() 这句代码,因为在 JVM(Java 虚拟机)执行这句代码的时候,要做好几件事情,而 JVM 为了优化代码,有可能造成做这几件事情的执行顺序是不固定的,从而造成错误。(为了不把问题更加复杂化,这里没有深入讲解在 JVM 中具体是怎么回事,有兴趣的同学可以点击 这里 自行了解下。)

DCL懒汉式

这个时候,我们需要给实例加一个 volatile 关键字,它的作用就是防止编译器自行优化代码。最后,我们的“双重检验锁”版本终于出炉了。

// 双重检验锁
public class Wife {
    private volatile static Wife wife;

    private Wife() { }

    public static Wife getWife() {
        if (wife == null) {
            synchronized(Wife.class) {
                if (wife == null) {
                    wife = new Wife();
                }
            }
        }

        return wife;
    }
}

volatile的内存屏障在单例模式是用的最多。(DCL懒汉式)

需禁止指令重排

private volatile static Lazy lazy;
package com.gs.single;

import sun.lwawt.macosx.CThreading;

/**
 * @author admin
 * @date 2021/8/28 1:54 下午
 */
public class Lazy {
    private Lazy() {
        System.out.println(Thread.currentThread().getName());
    }

    private volatile static Lazy lazy;

    public static Lazy getInstance() {
        // 双重检测锁模式的懒汉式单例 DCL懒汉式
        if (lazy == null) {
            synchronized (Lazy.class){
                if (lazy == null) {
                    lazy = new Lazy();  // 不是原子性操作
                    /*
                    1. 分配内存空间
                    2. 执行构造方法,初始化对象
                    3. 把这个对象指向这个空间

                    可能发生指令重排现象
                    如132
                     */
                }
            }
        }
        return lazy;
    }

    // 多线程并发
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                Lazy.getInstance();
            }).start();
        }
    }
}

禁止通过反射破坏单例模式

package com.gs.single;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * @author admin
 * @date 2021/8/28 1:54 下午
 */
public class Lazy {
    private Lazy() {

        // 禁止反射破坏单例
        synchronized (Lazy.class) {
            if (lazy != null) {
                throw new RuntimeException("不要试图使用反射破坏异常");
            }
        }

        System.out.println(Thread.currentThread().getName());
    }

    private volatile static Lazy lazy;

    public static Lazy getInstance() {
        // 双重检测锁模式的懒汉式单例 DCL懒汉式
        if (lazy == null) {
            synchronized (Lazy.class){
                if (lazy == null) {
                    lazy = new Lazy();  // 不是原子性操作
                    /*
                    1. 分配内存空间
                    2. 执行构造方法,初始化对象
                    3. 把这个对象指向这个空间

                    可能发生指令重排现象
                    如132
                     */
                }
            }
        }
        return lazy;
    }


    // 反射 破坏私有构造器
    public static void main(String[] args) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {

        Lazy instance1 = Lazy.getInstance();
        Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        Lazy instance = declaredConstructor.newInstance(null);

        System.out.println(instance1);

        System.out.println(instance);

    }
}

但是,在正常的实例化之前还是可以通过反射破坏单例模式。

于是将构造器的反射判断更改为信号灯模式,其中flag可以更改成密钥之类。

package com.gs.single;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * @author admin
 * @date 2021/8/28 1:54 下午
 */
public class Lazy {
    // 信号灯
    private static boolean flag = false;

    private Lazy() {
        // 禁止反射破坏单例
        synchronized (Lazy.class) {
            if (!flag) {
                flag = !flag;
            } else{
                throw new RuntimeException("不要试图使用反射破坏异常");
            }
        }

        System.out.println(Thread.currentThread().getName());
    }

    private volatile static Lazy lazy;

    public static Lazy getInstance() {
        // 双重检测锁模式的懒汉式单例 DCL懒汉式
        if (lazy == null) {
            synchronized (Lazy.class){
                if (lazy == null) {
                    lazy = new Lazy();  // 不是原子性操作
                    /*
                    1. 分配内存空间
                    2. 执行构造方法,初始化对象
                    3. 把这个对象指向这个空间

                    可能发生指令重排现象
                    如132
                     */
                }
            }
        }
        return lazy;
    }


    // 反射 破坏私有构造器
    public static void main(String[] args) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {

        Lazy instance1 = Lazy.getInstance();
        Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        Lazy instance = declaredConstructor.newInstance(null);

        System.out.println(instance1);

        System.out.println(instance);

    }
}

但还是有被反编译破解的可能。

道高一尺,魔高一丈。

newInstance源码:

@CallerSensitive
public T newInstance(Object ... initargs)
  throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
  if (!override) {
    if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
      Class<?> caller = Reflection.getCallerClass();
      checkAccess(caller, clazz, null, modifiers);
    }
  }
  if ((clazz.getModifiers() & Modifier.ENUM) != 0)
    throw new IllegalArgumentException("Cannot reflectively create enum objects");
  ConstructorAccessor ca = constructorAccessor;   // read volatile
  if (ca == null) {
    ca = acquireConstructorAccessor();
  }
  @SuppressWarnings("unchecked")
  T inst = (T) ca.newInstance(initargs);
  return inst;
}

可以发现如果是枚举类型,则会返回错误信息:”不能使用反射破坏枚举“。

5.静态内部类

上面的方法,修修补补,实在是太复杂了... 而且 volatile 关键字在某些老版本的 JDK 中无法正常工作。咱们得换一种方法,即 “静态内部类”。这种方式,利用了 JVM 自身的机制来保证线程安全,因为 WifeHolder 类是私有的,除了 getWife() 之外没有其它方式可以访问实例对象,而且只有在调用 getWife() 时才会去真正创建实例对象。(这里类似于 “懒汉模式”)

// 静态内部类
public class Wife {
    private static class WifeHolder {
        private static final Wife wife = new Wife();
    }

    private Wife() { }

    public static Wife getWife() {
        return WifeHolder.wife;
    }
}
package com.gs.single;

/**
 * @author admin
 * @date 2021/8/28 2:11 下午
 */
public class Holder {
    private Holder() {}

    private static Holder getInstance() {
         return InnerClass.HOLDER;
    }

    public static class InnerClass {
        private static final Holder HOLDER = new Holder();
    }
}

6.枚举

还不懂什么是枚举的,先 点这里 补补课。

如下,代码简直是简单得不能再简单了。我们可以通过 Wife.INSTANCE 来访问实例对象,这比 getWife() 要简单得多,而且创建枚举默认就是线程安全的,还可以防止反序列化带来的问题。这么优(niu)雅(bi)的方法,来自于新版 《Effective Java》 这本书。这种方式虽然不常用,但是最为推荐。

// 枚举
public enum Wife {
    INSTANCE;

    // 自定义的其他任意方法
    public void whateverMethod() { }
}
package com.gs.single;

import java.lang.reflect.Constructor;

/**
 * enum 本身也是一个Class类
 * @author admin
 * @date 2021/8/28 3:09 下午
 */
public enum EnumSingle {

    INSTANCE;

    public EnumSingle getInstance() {
        return INSTANCE;
    }
}

class Test {
    public static void main(String[] args) throws Exception{
        EnumSingle instance1 = EnumSingle.INSTANCE;
        EnumSingle instance2 = EnumSingle.INSTANCE;
        System.out.println(instance1);
        System.out.println(instance2);

        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        EnumSingle enumSingle = declaredConstructor.newInstance(null);
        System.out.println(enumSingle);
    }
}

通过idea自带的反编译,可以发现有空参构造器

admin@admindeMacBook-Pro single % javap -p EnumSingle.class 
Compiled from "EnumSingle.java"
public final class com.gs.single.EnumSingle extends java.lang.Enum<com.gs.single.EnumSingle> {
  public static final com.gs.single.EnumSingle INSTANCE;
  private static final com.gs.single.EnumSingle[] $VALUES;
  public static com.gs.single.EnumSingle[] values();
  public static com.gs.single.EnumSingle valueOf(java.lang.String);
  private com.gs.single.EnumSingle();
  public com.gs.single.EnumSingle getInstance();
  static {};
}

但是实际通过反射利用空参构造器来实例化对象的时候,会发现报错java.lang.NoSuchMethodException。

通过jd-gui反编译,发现没有构造器。

image-20210828161157563
Image

通过jad反编译,可以发现有一个有参构造器。

image-20210828161041303
Image

实际上,运行后正确存在的构造器只有最后一种。

通过有参反射尝试实例化

Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);

抛出异常

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    at com.gs.single.Test.main(EnumSingle.java:28)

至此枚举不能被实例化。

4. 应用

当你只需要一个实例对象的时候,就可以考虑使用单例模式。比如在资源共享的情况下,避免由于多个资源操作导致的性能或损耗等就可以使用单例模式。

5. References

Copyright © rootwhois.cn 2021-2022 all right reserved,powered by GitbookFile Modify: 2022-05-18 16:24:00

results matching ""

    No results matching ""