单例模式的定义
单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例(
隐藏所有构造方法),并提供一个全局访问点(getInstance),属于创建型模式
思考:单例因何出现?
单例模式的的适用场景
确保任何情况下都绝对只有一个实例
- ServletContenxt、ServletConfig、ApplicationContext、DBPool
饿汉式单例
- 在单例类首次加载时,就立即完成初始化并且创建实例对象,因此绝对线程安全,不存在访问安全的问题
- 饿汉式单例模式的一般写法
java
/**
* 优点:在类加载时进行初始化,执行效率高,性能高,没有任何的锁
* 缺点:某些情况下,可能会造成内存浪费
*/
public class HungrySingleton {
private static final HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton(){} //私有化构造方法
//提供一个全局访问点
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
- 也可使用静态代码快的方式进行初始化,二者没有本质上的差别
java
public class HungryStaticSingleton {
//先静态后动态
//先上,后下
//先属性后方法
private static final HungryStaticSingleton hungrySingleton;
static {
hungrySingleton = new HungryStaticSingleton();
}
private HungryStaticSingleton() {
}
public static HungryStaticSingleton getInstance() {
return hungrySingleton;
}
}
- 饿汉式单例模式适用于单例对象较少的情况。
- 这样写可以保证绝对线程安全、执行效率比较高。
- 但是它的缺点也很明显,就是所有对象类加载的时候就实例化。
- 这样一来,如果系统中有大批量的单例对象存在,那系统初始化是就会导致大量的内存浪费。也就是说不管对象用与不用都占着空间,浪费了内存。
为了解决饿汉式单例可能带来的内存浪费问题,于是就出现了懒汉式单例的写法,懒汉式单例模式的特点是,单例对象要在被使用时才会初始化
懒汉式单例
- 当被外部类调用时才创建实例
java
/**
* 优点:节省了内存
* 缺点:线程不安全
*/
public class LazySimpleSingleton {
private static LazySimpleSingleton instance;
private LazySimpleSingleton(){}
public static LazySimpleSingleton getInstance(){
if(instance == null){
instance = new LazySimpleSingleton();
}
return instance;
}
}
弊端:如果在多线程环境下,就会出现线程安全问题。
- 编写测试代码,模拟多线程并发异常
- 编写线程类
ExecutorThread
java
public class ExectorThread implements Runnable{
public void run() {
LazySimpleSingleton instance = LazySimpleSingleton.getInstance();
System.out.println(Thread.currentThread().getName() + ":" + instance);
}
}
- 编写测试类
java
public class LazySimpleSingletonTest {
public static void main(String[] args) {
Thread t1 = new Thread(new ExectorThread());
Thread t2 = new Thread(new ExectorThread());
t1.start();
t2.start();
System.out.println("End");
}
}
- 进行断点调试,模拟可能出现的三种结果



- 运行 debug

- 模拟情况一:两个线程按照正常顺序先后进入
getInstance方法,完成单例初始化 - 情况一预期:两次打印的单例为同一实例
- 预期结果解释:
Thread-0完成 初始化后,Thread-1直接返回Thread-0初始化的对象



- 模拟情况二:
Thread-0和Thread-1同时进入 if 代码快中,单例被两度实例化,后者覆盖前者,线程不安全,单实例被覆盖





- 模拟情况三:
Thread-0和Thread-1同时进入 if 代码快中,先后完成各种后续流程 - 预期结果:输出的两个实例不是同一个实例,线程不安全,产生多实例

懒汉式单例的优化
对于懒汉式单例存在线程不安全的问题可使用对
getInstance方法进行加锁的方案进行解决
- 方案一,使用
synchronized关键字
java
public synchronized static LazySimpleSingleton getInstance(){
if(instance == null){
instance = new LazySimpleSingleton();
}
return instance;
}
我们再来调试。当执行其中一个线程并调用 getlnstance0方法时,另一个线程在调用 getlnstance0方法,线程的状态由 RUNNING 变成了 MONITOR,出现阻塞。直到第一个线程执行完,第二个线程才恢复到 RUNNING 状态继续调用 getlnstance0方法,如下图所示。

上图完美地展现了 synchronized 监视锁的运行状态,线程安全的问题解决了。但是,用synchronized 加锁时,在线程数量比较多的情况下,如果 CPU分配压力上升,则会导致大批线程阻塞从而导致程序性能大幅下降。
那么,有没有一种更好的方式,既能兼顾线程安全又能提升程序性能呢?答案是肯定的。我们来看双重检查锁的单例模式
java
/**
* 优点:性能高了,线程安全了
* 缺点:可读性难度加大,不够优雅
*/
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton instance;
private LazyDoubleCheckSingleton(){}
public static LazyDoubleCheckSingleton getInstance(){
//检查是否要阻塞
if (instance == null) {
synchronized (LazyDoubleCheckSingleton.class) {
//检查是否要重新创建实例
if (instance == null) {
instance = new LazyDoubleCheckSingleton();
//涉及指令重排序的问题
//1.分配内存对象
//2.初始化对象
//3.设置instance指向的内存地址
}
}
}
return instance;
}
}
当第一个线程调用 getlnstance()方法时,第二个线程也可以调用。
当第一个线程执行到synchronized 时会上锁,第二个线程就会变成 MONITOR状态,出现阻塞。
此时,阻塞并不是基于整个 LazySimpleSingleton 类的阻塞,而是在 getlnstance0方法内部的阻塞,只要逻辑不太复杂,对于调用者而言感知不到。但是,用到 synchronized 关键字总归要上锁,对程序性能还是存在一定影响的。
是否更好的方案吗?当然有。我们可以从类初始化的角度来考虑,看下面的代码,采用静态内部类的方式:
java
/*
静态内部类单例写法
ClassPath : LazyStaticInnerClassSingleton.class
LazyStaticInnerClassSingleton$LazyHolder.class
优点:写法优雅,利用了Java本身语法特点,性能高,避免了内存浪费,不能被反射破坏
缺点:不优雅
*/
public class LazyStaticInnerClassSingleton {
//使用LazyInnerClassGeneral时,默认会初始化内部类,如果没使用,内部类不加载
private LazyStaticInnerClassSingleton() {
}
//每个关键字都不是多余的,static是为了使单例的空间共享,保证改方法不被重写,重载
private static LazyStaticInnerClassSingleton getInstance() {
//返回结果前,先加载内部类
return LazyHolder.INSTANCE;
}
//主类加载时,内部类默认不加载
private static class LazyHolder {
private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
}
}
这种方式兼顾了饿汉式单例模式的内存浪费问题和 synchronized 的性能问题。内部类一定是要在方法调用之前初始化,巧妙地避免了线程安全问题。由于这种方式比较简单,就不进行调试了。但是,该方式存在致命的安全问题------
能够被反射机制暴力破解,使单例失效
反射破坏单例模拟
上面介绍的单例模式的构造方法除了加上 private关键字,没有做任何处理。如果我们使用反射来调用其构造方法,再调用 qetlnstance()方法,应该有两个不同的实例。现在来看一段测试代码,以 LazyStaticInnerClassSingleton 为例:
java
public static void main(String[] args) {
try {
//很无聊的情况下,进行破坏
Class<?> clazz = LazyStaticInnerClassSingleton.class;
//通过反射获取私有的构造方法
Constructor<?> c = clazz.getDeclaredConstructor(null);
//暴力破解
c.setAccessible(true);
//执行初始化,创建两个实例,输出结果
Object instance1 = c.newInstance();
Object instance2 = c.newInstance();
System.out.println(instance1);
System.out.println(instance2);
System.out.println(instance1 == instance2);
// Enum
}catch (Exception e){
e.printStackTrace();
}
}
- 运行结果

显然,创建了两个不同的实例。那怎么办呢?我们来做一次优化。现在,我们在其构造方法中做些限制,一旦出现多次重复创建,则直接抛出异常。来看优化后的代码:
java
//在构造器中,天骄单例校验,抛出异常
private LazyStaticInnerClassSingleton() {
//避免暴力反射破坏
if (LazyHolder.INSTANCE != null) {
throw new RuntimeException("不允许非法访问");
}
}
- 运行,看效果

至此,自认为史上最牛的单例模式的实现方式便大功告成。但是,上面看似完美的单例写法还是有可能被破坏。
序列化破坏单例模拟
一个单例对象创建好后,有时候需要将对象序列化然后写入磁盘,下次使用时再从磁盘中读取对象并进行反序列化,将其转化为内存对象。反序列化后的对象会重新分配内存,即重新创建。如果序列化的目标对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例,来看一段代码:
java
public class SeriableSingleton implements Serializable {
//序列化
//把内存中对象的状态转换为字节码的形式
//把字节码通过IO输出流,写到磁盘上
//永久保存下来,持久化
//反序列化
//将持久化的字节码内容,通过IO输入流读到内存中来
//转化成一个Java对象
public final static SeriableSingleton INSTANCE = new SeriableSingleton();
private SeriableSingleton(){}
public static SeriableSingleton getInstance(){
return INSTANCE;
}
}
- 编写测试代码
java
public static void main(String[] args) {
SeriableSingleton s1 = null;
SeriableSingleton s2 = SeriableSingleton.getInstance();
FileOutputStream fos = null;
try {
fos = new FileOutputStream("SeriableSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (SeriableSingleton)ois.readObject();
ois.close();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
} catch (Exception e) {
e.printStackTrace();
}
}
- 运行,查看结果

从运行结果可以看出,反序列化后的对象和手动创建的对象是不一致的,实例化了两次,违背了单例模式的设计初衷。那么,我们如何保证在序列化的情况下也能够实现单例模式呢?其实很简单,只需要增加在SeriableSingleton 类中增加 readResolve0方法即可。
java
private Object readResolve(){ return INSTANCE;}

进入 ObjectinputStream 类的 readObject()方法代码如下,JDK 对该方法的描述大致如下几点
java
private final Object readObject(Class<?> type)
throws IOException, ClassNotFoundException
{
if (enableOverride) {
return readObjectOverride();
}
if (! (type == Object.class || type == String.class))
throw new AssertionError("internal error");
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(type, false);
handles.markDependency(outerHandle, passHandle);
ClassNotFoundException ex = handles.lookupException(passHandle);
if (ex != null) {
throw ex;
}
if (depth == 0) {
vlist.doCallbacks();
freeze();
}
return obj;
} finally {
passHandle = outerHandle;
if (closed && depth == 0) {
clear();
}
}
}
我们发现,在 readObject()方法中又调用了重写的 readObject0()方法。进入readObject0()方法代码如下:
java
private Object readObject0(Class<?> type, boolean unshared) throws IOException {
......
case TC_OBJECT:
if (type == String.class) {
throw new ClassCastException("Cannot cast an object to java.lang.String");
}
return checkResolve(readOrdinaryObject(unshared));
....
}
在 TC_OBJECT 中调用了 ObjectnputStream的readOrdinaryObject()方法
java
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
if (bin.readByte() != TC_OBJECT) {
throw new InternalError();
}
ObjectStreamClass desc = readClassDesc(false);
desc.checkDeserialize();
Class<?> cl = desc.forClass();
if (cl == String.class || cl == Class.class
|| cl == ObjectStreamClass.class) {
throw new InvalidClassException("invalid class descriptor");
}
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
.......
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, obj = rep);
}
}
return obj;
}
紧接着调用了 ObjectStreamClass 的 isInstantiable()方法,而isInstantiable()方法的代码如下:
java
boolean isInstantiable() {
requireInitialized();
return (cons != null);
}
上述代码非常简单,就是判断一下构造方法是否为空,构造方法不为空就返回 true。这意味着只要有无参构造方法就会实例化。
这时候其实还没有找到加上 readResolve0方法就避免了单例模式被破坏的真正原因。再回到ObjectlnputStream的 readOrdinaryObject0方法,继续往下看:
判断无参构造方法是否存在之后,,又调用了 hasReadResolveMethod()方法,来看代码
java
/**
* Returns true if represented class is serializable or externalizable and
* defines a conformant readResolve method. Otherwise, returns false.
*/
boolean hasReadResolveMethod() {
requireInitialized();
return (readResolveMethod != null);
}
上述代码逻辑非常简单,就是判断 readResolveMethod 是否为空,不为空就返回true。那么readResolveMethod 是在哪里赋值的呢?通过全局査找知道,在私有方法 ObjectStreamClass()中给readResolveMethod 进行了赋值,来看代码:
java
private ObjectStreamClass(final Class<?> cl) {
............
readResolveMethod = getInheritableMethod(
cl, "readResolve", null, Object.class);
.........
}
上面的逻辑其实就是通过反射找到一个无参的readResolve()方法,并且保存下来。现在回到ObjectlnputStream 的 readOrdinaryObject()方法继续往下看,如果readResolve()方法存在则调用invokeReadResolve0方法,来看代码:
java
Object invokeReadResolve(Object obj)
throws IOException, UnsupportedOperationException{
requireInitialized();
if (readResolveMethod != null) {
try {
return readResolveMethod.invoke(obj, (Object[]) null);
} catch (InvocationTargetException ex) {
Throwable th = ex.getCause();
if (th instanceof ObjectStreamException) {
throw (ObjectStreamException) th;
} else {
throwMiscException(th);
throw new InternalError(th); // never reached
}
} catch (IllegalAccessException ex) {
// should not occur, as access checks have been suppressed
throw new InternalError(ex);
}
} else {
throw new UnsupportedOperationException();
}
}
我们可以看到,在invokeReadResolve()方法中用反射调用了readResolveMethod 方法。通过 JDK 源码分析我们可以看出,虽然增加 readResolve()方法返回实例解决了单例模式被破坏的问题,但是实际上实例化了两次,只不过新创建的对象没有被返回而已。如果创建对象的动作发生频率加快,就意味着内存分配开销也会随之增大,难道真的就没办法从根本上解决问题吗?下面讲的注册式单例也许能帮助到你。
