单例模式的概念
单例模式(Singleton Pattern)的定义为:确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
单例模式是创建型模式。单例模式分为饿汉式单例和懒汉式单例,接下来我们对这两种类型做详细介绍。
饿汉式
饿汉式单例模式就是在类加载的时候就立即初始化,并且创建单例对象。不管你有没有用到,都先建好了再说。它绝对线程安全,在线程还没出现以前就实例化了,不可能存在访问安全问题。
优点:线程安全,没有加任何锁、执行效率比较高。
缺点:类加载的时候就初始化,不管后期用不用都占着空间,浪费了内存。
饿汉式单例的写法很简单,看下面代码:
还可以通过静态代码块的机制来实现:
这两种写法都很简单,都是创建了一个饿汉式的单例类。
饿汉式单例适合用在单例类比较少的情况下,在实际项目中,有可能会存在很多的单例类,如果我们都使用饿汉式单例的话,对内存的浪费会很大,所以,我们要学习更优的写法。
懒汉式
懒汉式,顾名思义就是实例在用到的时候才去创建,“比较懒”,用的时候才去检查有没有实例,如果有则直接返回,没有则新建。
下面看懒汉式的简单实现:
上面这种写法有一定概率会生成不同的对象,意味着这种写法不是线程安全的。原因如下:
假如有两个线程,线程A和线程B同时走到“if(lazy == null)”这个判断,因为lazy还没有没实例化过,所以两个线程判断的结果都是true,然后同时进入 if 代码块执行 new 操作,这时,线程A创建了一个实例,线程B也创建了一个实例,最后 return 的 lazy 肯定不是同一个对象,所以,这种写法是线程不安全的。
那我们要如何解决这个线程不安全的问题呢?最容易想到的方法就是加锁。我们把 getInstance() 方法进行加锁,看代码:
由于我们给该方法加上了 synchronized 锁,所以当线程A进入 getInstance() 方法的时候,线程B就只能在方法外等到线程A执行完这个方法之后才能进入该方法。由于线程A已经执行完该方法,所以此时 lazy 是不为null的,线程B就不会进入 if 代码块,最后返回的肯定是线程A创建的实例。
上面这种方式成功解决了线程安全问题,但在线程数量比较多的情况下,大量线程会阻塞在方法外部,导致程序性能下降。为了兼顾性能和线程安全问题,我们可以通过双重检查锁的方式来创建懒汉式的单例:
当第一个线程调用 getInstance()方法时,第二个线程也可以调用。当第一个线程执行到 synchronized 时会上锁,第二个线程就会变成 MONITOR 状态,出现阻塞。此时,阻塞并不是基于整 个 LazySimpleSingleton 类的阻塞,而是在 getInstance()方法内部的阻塞,只要逻辑不太复杂,对于 调用者而言感知不到。
但是,用到 synchronized 关键字总归要上锁,对程序性能还是存在一定影响的。难道没有更好的方案吗?显示是有的,我们可以从类初始化的角度来考虑,采用静态内部类的方式。看下面的代码:
这种方式兼顾了饿汉式单例模式的内存浪费问题和 synchronized 的性能问题。内部类一定是要在 方法调用之前初始化,巧妙地避免了线程安全问题。
反射破坏单例
饿汉式单例和懒汉式单例都是将构造方法私有化,防止在外部通过 new 来创建对象实例,以达到保证全局只有一个实例的效果。那我们思考一个问题:如果我们通过反射来调用其构造方法,在调用 getInstance() 方法得到 new 出来的实例,应该会存在两个不同的实例。现在来看一段测试代码,以上面的 LazyInnerClassSingleton 类为例:
为了防止这种情况的发生,我们在其构造方法中做一些限制,一旦出现多次创建,则直接抛出异常:
至此,最牛B的单例模式的实现就完成了!
序列化破坏单例
一个单例对象创建好后,有时候我们需要将对象序列化然后写入磁盘,下次使用时再从磁盘中读取对象 并进行反序列化,将其转化为内存对象。反序列化后的对象会重新分配内存,即重新创建。如果序列化 的目标对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例。来看一段代码:
看下测试代码:
运行结果为false。从运行结果可以看出,反序列化后的对象和手动创建的对象是不一致的,实例化了两次,违背了单例模式的设计初衷。那么,我们如何保证在序列化的情况下也能够实现单例模式呢?其实很简单,只需要增加 readResolve() 方法即可。来看优化后的代码:
这时,再执行上面的测试代码,输出结果就是 true 了。这是什么原因呢?我们就要看JDK反序列化的源码了。我们进入 ObjectInputStream 类的 readObject()方法, 代码如下:
我们发现,在 readObject() 方法中调用了 readObject0() 方法。进入 readObject0() 方法,代码如下:
我们看到 TC_OBJECT 中调用了 ObjectInputStream 的 readOrdinaryObject()方法,看源码:
我们发现调用了 ObjectStreamClass 的 isInstantiable()方法,而 isInstantiable()方法的代码如下:
上述代码非常简单,就是判断一下构造方法是否为空,构造方法不为空就返回 true。这意味着只要 有无参构造方法就会实例化。这时候其实还没有找到加上 readResolve()方法就避免了单例模式被破坏的真正原因。再回到 ObjectInputStream 的 readOrdinaryObject()方法,继续往下看:
判断无参构造方法是否存在之后,又调用了 hasReadResolveMethod() 方法,来看代码:
上述代码逻辑非常简单,就是判断 readResolveMethod 是否为空,不为空就返回 true。那么 readResolveMethod 是在哪里赋值的呢?通过全局查找知道,在私有方法 ObjectStreamClass()中给 readResolveMethod 进行了赋值,来看代码:
上面的逻辑其实就是通过反射找到一个无参的 readResolve()方法,并且保存下来。现在回到 ObjectInputStream 的 readOrdinaryObject()方法继续往下看,如果 readResolve()方法存在则调用 invokeReadResolve()方法,来看代码:
我们可以看到,在 invokeReadResolve()方法中用反射调用了 readResolveMethod 方法。最终返回我们添加的 readResolve() 方法中的实例。
虽然增加 readResolve()方法返回实例解决了单例模式被破坏的 问题,但是实际上实例化了两次,只不过新创建的对象没有被返回而已。
注册式单例模式
注册式单例模式又叫登记式单例模式,就是将每一个实例都登记到某一个地方,使用唯一的标识 获取实例。注册式单例模式有两种:一种为枚举式单例模式,另一种为容器式单例模式。
枚举式单例模式
先来看枚举式单例模式的写法:
使用序列化与反序列化,看两次拿到的对象是否一样:
运行之后,看到结果返回时true,说明序列化不会对枚举类型的单例产生破坏。那发射呢?我们看下测试反射破坏枚举单例的代码:
运行之后,会报一个异常:java.lang.NoSuchMethodException。意思是没找到无参的构造方法。这时候, 我们打开 java.lang.Enum 的源码,查看它的构造方法,只有一个 protected 类型的构造方法,代码如下:
那我们再来做一个下面这样的测试:
运行之后,会报一个这样的错:“Cannot reflectively create enum objects”,即不能用反射来创建枚举类型。其原因是JDK源码中,newInstance() 方法中做了判断,如果修饰符是 Modifier.ENUM 枚举类型,则直接抛出异常:
容器式单例
容器式单例的写法如下:
容器式单例模式适用于实例非常多的情况,便于管理。但它是非线程安全的。
点个关注吧,我会持续更新更多干货~~
领取专属 10元无门槛券
私享最新 技术干货