【Java】设计模式(一):玩转单例模式的十二种写法

苟日新,日日新,又日新。

当你点开这篇文章,欢迎你来到了设计模式的世界,也恭喜你离架构师又进了一步。

单例模式

作为一个程序员,经常听到有人说:程序员要什么对象?万物皆可为对象,对象这种东西不是想new多少就new多少吗?比如:

Wife wife1 = new Wife();
Wife wife2 = new Wife();
Wife wife3 = new Wife();
//...

然后坐拥后宫三千佳丽,直到对女色失去反应:
在这里插入图片描述
不好意思,那可能是你并没有听说过Java设计模式单例模式
在这里插入图片描述
那么,什么是单例模式呢?

可以说,单例模式确保了一个类在任何情况下只有一个实例,并提供了一个全局访问点。

单例模式(Singleton Pattern),又称单件模式,常常用来管理共享的资源,如数据库连接或者线程池。顾名思义,单例模式是用来创建独一无二的,只能有一个实例的对象。也就是说不管你怎么获取,全局都只有一个对象,就像鸣人的影分身一样,看起来有很多个,但其实只有一个。

在这里插入图片描述
单例模式可以说是所有的设计模式中类图最为简单的,因为它只有一个类。但是深入挖掘下去还是有不少乐趣在其中的。虽然说单例模式使得一个类在任何时刻都只有一个对象的特性让人感觉有点浪费,但其实一点也不浪费。事实上,很多对象我们都只需要一个,比如数据库连接、线程池或者注册表设置的对象等等。如果说,这类对象创建出了多个实例,反而会导致许多问题的产生,如:程序的行为异常、资源使用过度,或者产生不一致的结果。

接下来,让我们一起来探索它的写法吧!

饿汉式

饿汉式单例模式,顾名思义,它很饿,它需要马上创建这个独一无二的单件实例。也就是说这种单例模式的写法让JVM在加载这个类时马上创建唯一的单件实例

静态常量

/**
 * @author guqueyue
 * @Date 2020/5/30
 * 饿汉式单例模式(静态变量)
 * 1.构造器私有化
 * 2.本类内部创建对象实例
 * 3.提供一个公有的静态方法,返回实例对象
 **/
public class Hungry {

    private Hungry() {

    }

    private final static Hungry HUNGRY = new Hungry();

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

通过上面代码我们可以得知,实现单例模式中的静态常量懒汉式共需要三步:

  1. 构造器私有化(肯定不能把构造器公开让别人随便new呀)
  2. 在类内部创建一个静态常量,并创建一个实例对象赋值给这个静态常量。
  3. 提供一个公有的静态方法,并返回第二步中创建的这个静态常量。

静态代码块

其实静态代码块式的饿汉式单例模式跟之前的静态常量懒汉式单例模式相差无几,只不过是将类实例化的过程放到了静态代码块中而已,如:

/**
 * @author guqueyue
 * @Date 2020/5/31
 * 静态代码块饿汉式
 **/
public class Hungry2 {

    private Hungry2() {

    }

    private static Hungry2 uniqueInstance;

    // 在静态代码块中创建单例对象
    static {
        uniqueInstance = new Hungry2();
    }


    public static Hungry2 getInstance() {
        return uniqueInstance;
    }
}

Java源码示例

正所谓:实践是检验真理的唯一标准。作为社会主义新时代的接班人,理论当然要和实际相结合啦。学习设计模式最好能和Java或者Spring框架等的相关源码相结合。饿汉式单例模式实际运用可见如Java源码中的Runtime:

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

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class <code>Runtime</code> are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return  the <code>Runtime</code> object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}

	// other code...
}

优缺点分析

有句话说得好:世界上本没有垃圾,只是因为摆错了位置。只有明白了某件事物的优缺点,才能更好的找到这件事物的位置,而不至于让和氏璧成为一块破石头。那么饿汉式单例模式有什么优点呢?诶,帮你总结好了,优点如下:

  • 写法简单,就这么几行代码还不简单吗?
  • 线程安全,毕竟在类装载的时候就完成了实例化。

诶,这样看来饿汉式单例模式非常的完美,似乎没有什么优缺点嘛。非也非也,如我们来看下面的代码:

/**
 * @author guqueyue
 * @Date 2020/5/30
 **/
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;
    }
    
}

在上述代码中,这个类如果实例化会非常的占用内存空间。如果在类装载的时候就完成实例化,那么如果我们从头到尾都没有使用过这个对象就会造成内存空间很大的浪费。所以,这个时候饿汉式单例模式的缺点也就呼之欲出了:

  • 可能会造成内存空间的浪费

当然,这个时候可能有些小伙伴就会问了:要是会造成内存空间的浪费那我就先不创建这个类好了等到我用的时候在去创建这个类不就行了?但是这个时候我们就不能站在框架的使用者角度,而要站在一个框架的设计者——架构师的角度去思考问题了。架构师在设计一个框架的时候当然是把一个框架必备的功能都一一封装好,而我们在使用一个框架的时候,不可能每次都把所有的方法都用了个遍吧。就像之前的Runtime,我就使用的非常之少。也就是说,作为一个框架的设计者必定要封装好开发者一般所需要的所有功能,但是开发者在开发过程中不可能会使用到所有的功能

所以,这个时候延迟实例化,或者说是 懒加载(lazy loading) 的概念就被提出了。下文中的懒汉式单例模式闪亮登场。

懒汉式

lazy loading能够保证只有当我们使用到这个实例对象时,才装载这个实例对象,而不是开局就装载!

初始模式

开门见山,代码如下:

/**
 * @author guqueyue
 * @Date 2020/5/29
 **/
public class LazyMan{

    // 创建一个静态变量来记录Singeleton类的唯一实例
    private static LazyMan uniqueInstance;

    // 私有化构造器,保证只有Singelton类内才可以调用
    private LazyMan() {}

    public static LazyMan getInstance() {

        if (uniqueInstance == null) {
            uniqueInstance = new LazyMan();
        }

        return uniqueInstance;
    }
}

从代码中分析,单例模式中的懒汉式非常机智:只用了一个if非空判断就解决了可能会造成内存空间浪费的问题(只有当我调用Singleton.getInstance()方法时才会完成类实例化的装载)。但是我们在多线程环境下来测试一下:

/**
 * @author guqueyue
 * @Date 2020/5/29
 **/
public class LazyMan{

    // 创建一个静态变量来记录Singeleton类的唯一实例
    private static LazyMan uniqueInstance;

    // 私有化构造器,保证只有Singelton类内才可以调用
    private LazyMan() {
        //输出当前线程的名称,看这个私有化的构造器调用了多少次
        System.out.println(Thread.currentThread().getName() + " is ok!");
    }

    public static LazyMangetInstance() {

        if (uniqueInstance == null) {
            uniqueInstance = new LazyMan();
        }

        return uniqueInstance;
    }

    public static void main(String[] args) {
        // 开启十个线程
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
            	// 获取这个类的实例
                LazyMan.getInstance();
            }).start();
        }
    }
}

运行程序,控制台打印如下:
在这里插入图片描述
本来按照单例模式的设计思想,不管我获取多少次这个Singleton类的实例对象,只能有一个实例对象。也就是说,这个私有化的构造器只会被调用一次。但是我们很容易从这个的运行结果看出,私有化的构造器竟然被调用了四次。如果我们多次运行,还会惊喜地发现,每次调用私有化构造器的次数还不尽相同。所以,这个时候就暴露了懒汉式单例模式致命缺点

  • 线程不安全

当然这种情况也很好解释,无非是一个线程在判断uniqueInstance为null、即将准备创建对象实例给uniqueInstance赋值时,又有其他线程切了进来。而这个时候uniqueInstance还是为null,所以又会执行一次uniqueInstance = new Singleton();

if (uniqueInstance == null) {
	// 这时其他线程切了进来
	uniqueInstance = new LazyMan();
}

同步方法

那既然之前的懒汉式初始模式在多线程下会有问题,那我们自然而然的可以想到加锁的方式

代码如下:

/**
 * @author guqueyue
 * @Date 2020/5/31
 * 懒汉式单例模式,效率低
 **/
public class LazyMan2 {

    private static LazyMan2 uniqueInstance;

    private LazyMan2 () { }

    // 通过synchronized在静态方法上加锁,使得每个线程在进入这个方法前都要等待其他线程的离开
    public static synchronized LazyMan2 getInstance() {

        if (uniqueInstance == null) {

            uniqueInstance = new LazyMan2();
        }

        return uniqueInstance;
    }

    public static void main(String[] args) {

        // 开启十个线程
        for (int i = 0; i < 10; i++) {
            new Thread(() ->
                // 分别输出十个线程获取的实例对象的哈希码(同一个类的同一个对象哈希码是相同的)
                System.out.println(LazyMan2.getInstance().hashCode())
            ).start();
        }
    }
}

在上述代码中我们通过synchronized关键字在静态方法getInstance()上加锁,使得每个线程在进入这个方法前都要等待其他线程的离开。也就是说,不会同时有两个或以上的线程同时进入这个静态方法,自然也就不会有之前的线程安全问题了。话不多说,运行main方法,打印出十个线程中每个线程获取的实例对象的哈希码:
在这里插入图片描述
我们可以很简单的发现,打印出来的哈希码都是一样的。因为同一个类的同一个对象的哈希码是相同的(当然,同一个类的不同对象的哈希码都是不同的),所以这次看来似乎又完美了。但是,这次真的完美了吗?其实我们很轻易的可以分析得出,在第一个线程完成静态变量uniqueInstance的赋值后我们就不需要这把锁了,但是后面的线程还需要去申请得到这个锁。这个时候我们很容易得出,同步方法懒汉式单例模式很明显的一个缺点:

  • 效率低下

同步代码块

既然之前的同步方法懒汉式有问题,那我们就得继续探索了。我们不难分析出理论上有两种方式可以提高效率:要么想办法在第一个线程完成静态变量uniqueInstance的赋值后删除或者避开这把锁,要么减少锁的范围。前者很难实现,后者却很简单。所以,我们又可以蹭蹭蹭写下以下的代码:

/**
 1. @author guqueyue
 2. @Date 2020/5/31
 3. 懒汉式单例模式
 4. 同步代码块,线程不安全
 **/
public class LazyMan3 {

    private static LazyMan3 uniqueInstance;

    private LazyMan3() {
        System.out.println(Thread.currentThread().getName() + " is ok");
    }

    public static LazyMan3 getInstance() {

        if (uniqueInstance == null) {

            // 试图通过减少同步代码块的方式提高效率
            synchronized (LazyMan3.class) {
                uniqueInstance = new LazyMan3();
            }
        }

        return uniqueInstance;
    }

    public static void main(String[] args) {
		
		// 开启十个线程
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                	// 获取LazyMan3的实例对象
                    LazyMan3.getInstance();
                }
            }).start();
        }

    }
}

一运行,控制台打印如下:
在这里插入图片描述
这下又傻眼了:效率是提高了,但是线程又不安全了。其实为什么线程不安全,跟之前懒汉式初始模式的解释差不多:无非是一个线程在判断uniqueInstance为null、在加锁之后即将准备创建对象实例给uniqueInstance赋值时,又有其他线程切了进来。而这个时候uniqueInstance还是为null,所以还是会执行一次uniqueInstance = new LazyMan3();

DCL模式

那,怎么办呢?这个时候双重检测锁懒汉式就应运而生了:

/**
 5. @author guqueyue
 6. @Date 2020/5/31
 7. 懒汉式单例模式
 8. 双重检测锁: 效率高、线程安全且避免了内存浪费,但是不易理解(对新手不太友好)
 **/
public class LazyMan4 {
	
	// volatile关键字可以确保uniqueInstance变量被初始化成LazyMan4实例时,多个线程正确处理uniqueInstance变量。
    private volatile static LazyMan4 uniqueInstance;

    private LazyMan4() {
        System.out.println(Thread.currentThread().getName() + " is ok");
    }

    public static LazyMan4 getInstance() {
		
		// 判断后续线程是否需要继续加锁
        if (uniqueInstance == null) {

            // 试图通过减少同步代码块的方式提高效率
            synchronized (LazyMan4.class) {
                // 在给实例对象uniqueInstance赋值时,再判断一次是否为空
                if (uniqueInstance == null) {
                    uniqueInstance = new LazyMan4();
                }
            }
        }

        return uniqueInstance;
    }

    public static void main(String[] args) {

        // 开启十个线程
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    // 获取LazyMan4的实例对象
                    LazyMan4.getInstance();
                }
            }).start();
        }
    }
}

通过以上代码可以发现,我们只需要在uniqueInstance = new LazyMan4();前再判断一次uniqueInstance是否为空就好了。因为需要判断两次uniqueInstance是否为空,并且加了一把锁,所以这种写法被称为双重检测锁的单例模式,也称DCL(Double Check Lock)单例模式。

当然,这个时候细心的同学又发现了静态变量uniqueInstance前的volatile又是什么鬼?为什么要加这个?不要急,下一章节给你娓娓道来。

volatile

volatile是Java虚拟机提供的轻量级的同步机制,有三个特性:

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排

而这里的volatile就是为了禁止指令重排。在多线程环境下,指令重排是Java底层为了性能和效果所导致的一种现象。那么到底什么是指令重排呢?就比如这一行代码:

 uniqueInstance = new LazyMan4();

看起来这行代码只会执行一次,似乎是一个原子性操作,但在底层却可以分为三步:

memory = allocate(); // 1.分配内存空间
instance(memory); // 2.执行构造方法,初始化对象
uniqueInstance = memory; // 3.设置uniqueInstance对象指向刚刚分配的memory空间,此时uniqueInstance!= null

但是由于步骤2和步骤3不存在数据依赖关系,所以执行顺序很可能由1 -> 2 -> 3而变为1 -> 3 -> 2,也就是:

memory = allocate(); // 1.分配内存空间
uniqueInstance = memory; // 3.设置uniqueInstance 对象指向刚刚分配的memory空间,此时uniqueInstance!= null
instance(memory); // 2.执行构造方法,初始化对象

如果是单线程的话,其实123132的执行顺序执行结果都不会改变。但是指令重排只会保证串行语义的执行一致性,也就是单线程下的一致性,而并不能保证多线程间的语义一致性。那么,到底会有什么问题呢?我们来看这一段代码:

 public static LazyMan4 getInstance() {
		
		// 判断后续线程是否需要继续加锁
        if (uniqueInstance == null) {

            // 试图通过减少同步代码块的方式提高效率
            synchronized (LazyMan4.class) {
                // 在给实例对象uniqueInstance赋值时,再判断一次是否为空
                if (uniqueInstance == null) {
                    uniqueInstance = new LazyMan4();
                }
            }
        }

        return uniqueInstance;
    }

比如这个时候,A线程执行到了uniqueInstance = new LazyMan4();这一行代码,发生了指令重排,于是就先执行了第三步(将对象指向分配的内存空间)。这个时候uniqueInstance就已经不为null了。如果这个时候B线程切了进来,发现uniqueInstance就不为null,于是就直接返回uniqueInstance对象。而这个时候uniqueInstance对象却并没有初始化完成。所以,这个时候volatile的作用就显现出来了。虽然指令重排可能只有千万分之一的概率,但是在高并发环境下不容小觑,该加还是得记得加的!

静态内部类

通过之前的思考,是不是感觉有点小刺激呢?那我偷偷告诉你一个秘密:其实实现lazy loading根本不需要这么麻烦。利用静态内部类的特性,轻松帮你实现懒加载单例模式,并且还线程安全哦。代码如下:

/**
 * @author guqueyue
 * @Date 2020/5/30
 * 静态内部类
 * 1.类装载的时候,静态内部类是不会被装载(懒加载,以外部类的装载不会导致内部类的装载)
 * 2.当调用getInstance()方法的时候,会导致静态内部类被装载,而且只会被装载一次
 * 3.在装载的时候线程是安全的。(JVM底层类装载机制)
 **/
public class Holder {
	
	// 私有化构造器
    private Holder() {

    }
	
	// 提供一个全局访问点,返回静态内部类中的静态常量
    public static Holder getInstance() {
        return InnerClass.HOLDER;
    }
	
	// 在静态内部类中创建一个静态常量并将一个外部类的实例赋值给它。
    public static class InnerClass {
        private static final Holder HOLDER = new Holder();
    }
}

其实静态内部类的形式跟静态常量饿汉式很像,只不过它将创建静态常量并赋值的工作放在了静态内部类里。只不过这种呢方式利用JVM类装载外部类的时候不会装载内部类,并且在装载的时候JVM底层保证了线程安全的方式是不是很炫酷呢?似乎看来,静态内部类的方式又非常完美了,但是真的完美了吗?我们试图用反射再来获取一下Holder的实例:

public static void main(String[] args) throws Exception {
	   // 用提供的唯一全局访问点获取实例对象
       Holder instance = Holder.getInstance();
        // 获取Holder的反射对象
        Class<Holder> clazz = Holder.class;
        // 通过反射对象获取Holder的构造器
        Constructor<Holder> declaredConstructor = clazz.getDeclaredConstructor();
        // 私有访问授权
        declaredConstructor.setAccessible(true);
        // 创建Holder的实例对象
        Holder instance2 = declaredConstructor.newInstance();
        System.out.println(instance == instance2);
    }

运行main方法,控制台结果:
在这里插入图片描述
完了,这下有创建出了两个不同的实例,单例模式就这样被简简单单的破解了。不过没关系,有破解之法就有防守之道。我们只需要在私有化的构造器里面加个判断,若是静态常量HOLDER不为null就抛出一个自定义异常就ok了:

 // 私有化构造器
    private Holder() {

        if (InnerClass.HOLDER != null) {
            throw new RuntimeException("小朋友,不要试图用反射搞破坏!");
        }
    }

再次运行main方法:
在这里插入图片描述
这下再也不能用反射创建实例对象了,开发者就得乖乖的用我们提供的唯一全局访问点getInstance()方法了。这下看来似乎又完美了,但是这种写法虽然干脆利落,却直接封杀了反射的可能性。如我只很单纯的想用反射创建一个对象而已:

public static void main(String[] args) throws Exception {

        // 获取Holder的反射对象
        Class<Holder> clazz = Holder.class;
        // 通过反射对象获取Holder的构造器
        Constructor<Holder> declaredConstructor = clazz.getDeclaredConstructor();
        // 私有访问授权
        declaredConstructor.setAccessible(true);
        // 创建Holder的实例对象
        Holder instance = declaredConstructor.newInstance();
        System.out.println(instance);
    }

运行程序,结果:
在这里插入图片描述
本来按照常规思路,你不让我创建多个对象,那我只创建一个总行了吧。结果,这种方式很不人性化的告诉我不行。其实原因很简单:当用反射第一次调用私有化构造器时,内部类静态常量InnerClass.HOLDER的非空判断会导致内部类静态常量被装载,即触发这行代码:

private static final Holder HOLDER = new Holder();

然后会导致Holder私有化构造器的第二次调用。当第二次调用的时候,内部类静态常量InnerClass.HOLDER已经不为空了,这个时候就会抛出异常。那怎么办呢?既然在静态内部类单例模式的私有化构造器中判断方式会造成构造器的两次调用,从而导致反射第一次都无法实现类的实例化。那么,我们自然而然的可以想到之前的双重检测锁懒汉式。

三重检测锁

回到之前的双重检测锁懒汉式,然后在私有构造器里加入静态变量uniqueInstance的非空判断,并在静态变量uniqueInstance不为空时抛出异常。这下,静态变量uniqueInstance的非空判断不会导致私有化构造器的再次调用了,似乎不会有之前的问题了。代码如下:

/**
 * @author guqueyue
 * @Date 2020/5/31
 * 懒汉式单例模式
 * 双重检测锁
 **/
public class LazyMan4 {

    // volatile关键字可以确保uniqueInstance变量被初始化成LazyMan4实例时,多个线程正确处理uniqueInstance变量。
    private volatile static LazyMan4 uniqueInstance;
	
	// 私有化构造器
    private LazyMan4() {
        synchronized (LazyMan4.class) {
            if (uniqueInstance != null) {
                throw new RuntimeException("小朋友,不要试图用反射搞破坏!");
            }
        }
    }

    public static LazyMan4 getInstance() {

        // 可以减少锁的损耗
        if (uniqueInstance == null) {

            // 试图通过减少同步代码块的方式提高效率
            synchronized (LazyMan4.class) {

                // 在给实例对象uniqueInstance赋值时,再判断一次是否为空
                if (uniqueInstance == null) {
                    uniqueInstance = new LazyMan4();
                }
            }
        }

        return uniqueInstance;
    }

    public static void main(String[] args) throws Exception {
        // 用提供的全局访问方法获取实例对象
        LazyMan4 instance = LazyMan4.getInstance();
        // 获取LazyMan4的反射对象
        Class<LazyMan4> clazz = LazyMan4.class;
        // 获取LazyMan4的构造方法
        Constructor<LazyMan4> declaredConstructor = clazz.getDeclaredConstructor();
        // 给私有化构造方法授权
        declaredConstructor.setAccessible(true);
        // 用反射获取实例对象
        LazyMan4 instance2 = declaredConstructor.newInstance();
        System.out.println(instance == instance2);
    }
}

运行程序,控制台结果如下:
在这里插入图片描述
这下似乎完美无缺了,但是我又要灵魂发问了:如果我两次对象都是用反射获取的呢?如:

 public static void main(String[] args) throws Exception {
        // 获取LazyMan4的反射对象
        Class<LazyMan4> clazz = LazyMan4.class;
        // 获取LazyMan4的构造方法
        Constructor<LazyMan4> declaredConstructor = clazz.getDeclaredConstructor();
        // 给私有化构造方法授权
        declaredConstructor.setAccessible(true);
        // 用反射获取两次实例对象
        LazyMan4 instance = declaredConstructor.newInstance();
        LazyMan4 instance2 = declaredConstructor.newInstance();
        System.out.println(instance == instance2);
    }

一运行:
在这里插入图片描述
这下,又和单例模式说再见了,似乎有点小尴尬。然而,有破解之法就有解决之道。具体该如何破解,请往下看!

再次升级

之前我们说到连续用反射获取两次实例对象无法正确抛出异常,那我们抛出异常的判断方式可能需要改一下了,如:

/**
 * @author guqueyue
 * @Date 2020/5/31
 * 懒汉式单例模式
 * 双重检测锁
 **/
public class LazyMan4 {
	
	// 设置一个标记变量,为true时就抛出异常
    private static boolean flag = false;

    // volatile关键字可以确保uniqueInstance变量被初始化成LazyMan4实例时,多个线程正确处理uniqueInstance变量。
    private volatile static LazyMan4 uniqueInstance;

    private LazyMan4() {
        synchronized (LazyMan4.class) {
            if (flag == false) {
                flag = true;
            } else {
                throw new RuntimeException("小朋友,不要试图用反射搞破坏!");
            }
        }
    }

    public static LazyMan4 getInstance() {

        // 可以减少锁的损耗
        if (uniqueInstance == null) {

            // 试图通过减少同步代码块的方式提高效率
            synchronized (LazyMan4.class) {

                // 在给实例对象uniqueInstance赋值时,再判断一次是否为空
                if (uniqueInstance == null) {
                    uniqueInstance = new LazyMan4();
                }
            }
        }

        return uniqueInstance;
    }
}

再试图用反射连续获取两次实例对象:

 public static void main(String[] args) throws Exception {
        // 获取LazyMan4的反射对象
        Class<LazyMan4> clazz = LazyMan4.class;
        // 获取LazyMan4的构造方法
        Constructor<LazyMan4> declaredConstructor = clazz.getDeclaredConstructor();
        // 给私有化构造方法授权
        declaredConstructor.setAccessible(true);
        // 用反射获取实例对象
        LazyMan4 instance = declaredConstructor.newInstance();
        LazyMan4 instance2 = declaredConstructor.newInstance();
        System.out.println(instance == instance2);
}

运行程序,控制台输出:
在这里插入图片描述
这下,似乎又完美了。然而道高一尺魔高一丈,这点小小的计策在强大的反射面前又怎能生效。既然flagtrue才抛出异常,那我直接用反射将flag手动改为false不就行了:

public static void main(String[] args) throws Exception {
        // 获取LazyMan4的反射对象
        Class<LazyMan4> clazz = LazyMan4.class;
        // 获取LazyMan4的构造方法
        Constructor<LazyMan4> declaredConstructor = clazz.getDeclaredConstructor();
        // 给私有化构造方法授权
        declaredConstructor.setAccessible(true);
        // 用反射获取实例对象
        LazyMan4 instance = declaredConstructor.newInstance();

        // 用反射获取flag属性,并且重新将flag赋值为false
        Field flag = clazz.getDeclaredField("flag");
        flag.setAccessible(true);
        flag.set(instance, false);

        LazyMan4 instance2 = declaredConstructor.newInstance();
        System.out.println(instance == instance2);
}

运行程序:
在这里插入图片描述
然后,我们很崩溃的发现,单例模式又失效了。所幸,标记变量的属性名肯定不能像flag一样这么简单易猜,而且我们完全可以使用像32位随机字符串如UUID之类的复杂名称并且做加密处理。不过这么想想,好像还是静态内部类的方式直接封杀反射的方式显得更好一些。但是静态内部类的单例写法似乎有些复杂了,那么有没有更为简单的写法呢?当然有!请看下文。

枚举

欢迎来到枚举的单例模式,枚举形式的单例模式也是Java四大名著中《Effective Java》里面的推荐写法。

代码如下:

/**
 * @author guqueyue
 * @Date 2020/6/5
 * 枚举自带单例模式
 **/
public enum EnumSingleton {

    INSTANCE;

    public static EnumSingleton getInstance() {

        return 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;
    }

仔细观察,里面有着这样两行代码:

if ((clazz.getModifiers() & Modifier.ENUM) != 0)
	throw new IllegalArgumentException("Cannot reflectively create enum objects");

其实跟我们之前静态内部类单例模式中私有构造器里面的写法有着异曲同工之妙:

if (InnerClass.HOLDER != null) {
	throw new RuntimeException("小朋友,不要试图用反射搞破坏!");
}

光说不练假把式,让我们来实验一下吧:

public static void main(String[] args) throws Exception {
        // 获取枚举EnumSingleton的反射对象
        Class<EnumSingleton> clazz = EnumSingleton.class;
        // 利用反射对象获取EnumSingleton的构造器
        Constructor<EnumSingleton> declaredConstructor = clazz.getDeclaredConstructor();
        // 私有访问授权
        declaredConstructor.setAccessible(true);
        // 利用反射获得的构造器实现EnumSingleton的实例化
        EnumSingleton instance = declaredConstructor.newInstance();
    }

一运行,控制台结果如下:
在这里插入图片描述
好像成功抛出了异常,仔细一看这个抛出的异常似乎有点不对劲,怎么是这个:

Exception in thread "main" java.lang.NoSuchMethodException: com.guqueyue.singleton.EnumSingleton.<init>()

这个异常告诉我,没有构造方法,而不是不能用反射创建枚举对象:

Cannot reflectively create enum objects

那怎么回事呢?我们查看一下枚举继承的抽象类Enum的底层源码,发现其中会有这么一个带双参的构造方法,而不是无参:

 protected Enum(String name, int ordinal) {
	this.name = name;
	this.ordinal = ordinal;
}

哦,那我们利用反射获取这个双参的构造方法就好了。再次修改代码如下:

public static void main(String[] args) throws Exception {
	// 获取枚举EnumSingleton的反射对象
	Class<EnumSingleton> clazz = EnumSingleton.class;
	// 利用反射对象获取EnumSingleton的构造器
	Constructor<EnumSingleton> declaredConstructor = clazz.getDeclaredConstructor(String.class, int.class);
	// 私有访问授权
	declaredConstructor.setAccessible(true);
	// 利用反射获得的构造器实现EnumSingleton的实例化
	EnumSingleton instance = declaredConstructor.newInstance("古阙月", 666);
}

再次运行,结果如下:
在这里插入图片描述
终于得到了我们想要的异常。这样看来,似乎又完美了,代码简洁优雅线程安全(不信的,可以自己去试试),并且Java底层保证了防止反射破解。但是我又要诚心诚意的发问了:这次真的完美了吗?我们来研究一下枚举的底层实现,我们在再一次点开枚举继承的抽象类Enum的底层源码,并且找到其中的valueOf()方法:

public static <T extends Enum<T>> T valueOf(Class<T> enumType,
                                                String name) {
	T result = enumType.enumConstantDirectory().get(name);
	if (result != null)
		return result;
	if (name == null)
		throw new NullPointerException("Name is null");
	throw new IllegalArgumentException(
		"No enum constant " + enumType.getCanonicalName() + "." + name);
}

我们会发现这样一行代码:

T result = enumType.enumConstantDirectory().get(name);

点开这个枚举常量字典enumConstantDirectory,我们又会发现:

Map<String, T> enumConstantDirectory() {
	if (enumConstantDirectory == null) {
		T[] universe = getEnumConstantsShared();
		if (universe == null)
			throw new IllegalArgumentException(
				getName() + " is not an enum type");
		Map<String, T> m = new HashMap<>(2 * universe.length);
		for (T constant : universe)
			m.put(((Enum<?>)constant).name(), constant);
		enumConstantDirectory = m;
		}
	return enumConstantDirectory;
}
private volatile transient Map<String, T> enumConstantDirectory = null;

这个时候我们会发现枚举常量字典enumConstantDirectory为Map<String, T>类型,其中key为String类型,而value是一个泛型对象。其中key就是由我们自定义的,如上文中的INSTANCE;。所以,枚举是通过这个String类型的key,去拿到这个value,这才保证了单例模式的实现。但是我们发现了枚举常量字典中的常量二字。既然是常量的话,那么就意味着在类加载的时候就会赋值。这个时候,我们尴尬的发现,我们又回到了最初的起点 —> 饿汉式单例模式。那这个时候怎么办呢?欲知解决办法,请听下回分解!

容器式单例

之前我们说到枚举单例模式可能会浪费内存空间的问题,那么我们这里给出解决之道。代码如下:

/**
 * @author guqueyue
 * @Date 2020/6/6
 * 解决了枚举单例模式可能造成内存浪费的问题
 * 注册式单例,Spring中的做法
 **/
public class ContainerSingleton {

    // 私有化构造器
    private ContainerSingleton() {

    }

    // 声明一个Map
    private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();

    public static Object getInstance(String className) {
        synchronized (ioc) {
            if (!ioc.containsKey(className)) {
                // 如果map中不存在这个全限定类名的key,那么需要放入新的数据
                Object obj = null;
                try {
                    // 利用全限定类名获取反射对象,再实现类的实例化
                    obj = Class.forName(className).newInstance();
                    // 把全限定类名以及对象,以key-vaule的形式放入map中
                    ioc.put(className, obj);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return obj;
            } else {
                // 如果map中存在这个全限定类名的key,直接通过这个key返回对应的对象
                return ioc.get(className);
            }
        }
    }
}

这种写法可以说是兼顾了之前的所有写法的优点。要不然,也不会为大名鼎鼎的Spring框架所采用!刚开始看可以会有点懵,但是渐渐的就能发现其中的妙处!

猜你喜欢

转载自blog.csdn.net/Qizhi_Hu/article/details/106451236