单例模式被称为最简单的设计模式,也是应用场景非常多的一种设计模式,如计数器、线程池、连接池等都需要用到单例模式。本文从最基本的单线程懒汉式单例模式开始,对单例模式的实现方式逐步深入和优化,一共总结了六种单例模式。
1. 简单的懒汉式单例模式
最基本的单例模式模型,通过私有化构造方法限制对象的实例化,使用静态私有化的实例变量和获取单例对象的公共方法来控制实例对象的单例。
代码展示:
public class Singleton1{
//单例的实例变量
private static Singleton1 instance = null;
//私有化构造方法,保证不被其他类使用构造器实例化
private Singleton1() {
}
//获取单例模式对象
public static Singleton1 getInstance() {
if (instance == null) {
System.out.println("我是简单懒汉式!");
instance = new Singleton1();
}
return instance;
}
}
存在的问题:
很容易看出这种模型在多线程的情况下是不安全的,当有两个线程同时进入 if (instance == null) 时,就会创建两个实例。
解决方案:
通过加一个同步锁来进行优化。
2. 简单的懒汉式单例模式(加同步锁)
通过在创建实例对象的方法上加同步锁使线程互斥,保证单例的实现。
代码展示:
public class Singleton2 {
//单例的实例变量
private static Singleton2 instance = null;
//私有化构造方法,保证不被其他类使用构造器实例化
private Singleton2() {
}
//获取单例模式对象,一次只能被一个线程调用
public static synchronized Singleton2 getInstance() {
if (instance == null) {
System.out.println("我是简单同步懒汉式!");
instance = new Singleton2();
}
return instance;
}
}
存在的问题:
直接在获取实例的方法上加同步锁,会导致每一次调用getInstance()方法获取实例时,都需要对锁进行争夺,非常影响多线程的效率。
解决方案:
实际上我们只需要对创建实例对象的过程进行同步就行了,可以通过双重锁定来优化。
3. 双重锁定的懒汉式单例模式
仅对实例化对象的过程进行同步。当多个线程同时调用getInstance()获取实例化对象时,即使同时通过了 if (instance == null) 判断,在进行实例化对象的创建时,也只能有一个线程进行创建,后续线程获取锁之后再进行一次 if (instance == null) 判断,保证对象的单例。同时在实例化对象创建之后,多个线程获取对象时不再需要争夺锁。
代码展示:
public class Singleton3 {
//单例的实例变量
private static Singleton3 instance = null;
//私有化构造方法,保证不被其他类使用构造器实例化
private Singleton3() {
}
//获取单例模式对象
public static Singleton3 getInstance() {
if (instance == null) { // 1
//静态方法中的锁对象为类的字节码(class)对象
synchronized (Singleton3.class) { // 2
if (instance == null) {
System.out.println("我是双重锁定懒汉式!");
instance = new Singleton3(); // 3
}
}
}
return instance;
}
}
存在的问题:
似乎线程安全问题和性能损耗问题同时得到了解决,但是很遗憾,JVM脾气很大的。
java内存模型中有一个叫“无序写”(out-of-order writes)的机制,在Java指令中创建对象和赋值操作是分开进行的。在执行instance = new Singleton3()实例化对象时,其实分为以下几步:
1. 分配堆内存
2. 实例化对象new Singleton3(),存放在堆内存中。
3. 将堆内存的地址值赋予栈中的变量instance
而JVM并不保证这些操作的先后顺序,就可能会出现以下顺序的情况:
1. 分配堆内存
2. 将堆内存的地址值赋予栈中的变量instance
3. 实例化对象new Singleton3(),存放在堆内存中。
这就会导致两个线程A、B并发调用getInstance()获取实例化对象时,可能会发生以下情况:
1. 线程A运行到1,instance为null
2. 线程A运行到3,开始实例化对象
3. 线程A给将要实例化的对象分配了一个堆内存
4. 线程A将堆内存的地址值赋予栈中的变量instance,此时instance中有了地址值,不为 null,但地址值对应的堆内存中还没有对象
5. 线程B运行到1,instance不为null,但地址值指向的堆内存中还没有对象
6. 线程B返回了instance,此时instance不为null,但也没有存储对象,调用该对象会出现问题
7. 线程A实例化对象new Singleton3(),存放在堆内存中,此时instance才能正常使用
总结:
在5、6这两步,即instance指向了一个堆内存,而堆内存还没来得及赋予对象的极短时间内,如果有另一个线程在这段时间里面获取到了instance并调用,则会出现问题,虽然在第七步执行后会一切正常,但在这一段极短的时间内仍存在隐患。
解决方案:
为消除隐患,我们可以通过静态内部类来创建实例。
4. 静态内部类实现的懒汉式单例模式
使用静态内部类的静态属性来实例化外部类对象,因为静态内部类不会随着外部类的加载而加载,而是在被调用时才加载,所以它仍然属于延迟加载的“懒汉式”,可以减缓内存的压力。同时,外部类的实例化交给内部类的静态属性加载,而JVM确保类的加载是线程安全的。在这里还进行了一个小小的优化,修改私有构造方法,防止反射入侵破坏单例。
代码展示:
public class Singleton4 {
//静态成员内部类
private static class SingletonHolder {
//单例变量
private static Singleton4 instance = new Singleton4();
}
//私有化构造方法,保证不被其他类使用构造器实例化
private Singleton4() {
//当私有化构造方法被反射入侵时,抛出异常
if (SingletonHolder.instance != null) {
throw new IllegalStateException();
}
}
//获取单例模式对象
public static Singleton4 getInstance() {
System.out.println("我是静态内部类实现的懒汉式!");
return SingletonHolder.instance;
}
}
存在的问题:
到这一步好像已经万事大吉了,但是我们知道,对象的创建一共有四种方法: new 、克隆、序列化、反射。
我们已经把new和反射破坏单例的路子堵死了,而克隆需要类实现 Cloneable 接口,并且重写继承自Object类的clone()方法为public权限,我们不主动对单例的类进行相关操作就可以避免。
最后还剩序列化:
序列化一是可以实现数据的持久化,二是可以对象数据进行远程传输,这些功能都是我们经常需要用到的,而要想实现这些功能就需要将对象序列化,这就必须实现 Serializable 接口。
《Effective Java》中第三条提到:
“如果在这个类的声明中加上了‘implements Serializable’的字样,它就不再是一个Singleton。”
这是因为当反序列化流从内存读出而组装的对象破坏了单例的规则,会创建新的对象。单例要求JVM只有一个对象,而通过反序化时就会产生一个克隆的对象,这就打破了单例的规则。
解决方案:
我们可以通过给类添加一个readResolve()方法来处理这个问题。
5. 静态内部类实现的懒汉式单例模式(优化反序列化)
在反序列化流ObjectInputStream的类中有readUnshared()方法,它的作用为:如果被反序列化的对象的类存在readResolve()这个方法,他会调用这个方法来返回指定的值,并且无视掉反序列化的值,即使那个字节码已经被解析。所以我们可以给单例对象的类手动添加一个readResolve()方法,来保证它不被反序列化破坏单例。
代码展示:
public class Singleton5 implements Serializable {
//静态成员内部类
private static class SingletonHolder {
//单例变量
private static Singleton5 instance = new Singleton5();
}
//私有化构造方法,保证不被其他类使用构造器实例化
private Singleton5() {
//当私有化构造方法被反射入侵时,抛出异常
if (SingletonHolder.instance != null) {
throw new IllegalStateException();
}
}
//获取单例模式对象
public static Singleton5 getInstance() {
System.out.println("我是优化了反序列化的静态内部类实现的懒汉式!");
return SingletonHolder.instance;
}
//反序列化流调用,返回指定的对象,防止反序列化破坏单例
private Object readResolve() throws ObjectStreamException {
System.out.println("调用了readResolve方法!");
return SingletonHolder.instance;
}
}
存在的问题&&解决方案:
手动添加readResolve()方法在某些情况下会比较复杂,虽然算不上什么问题,但是还有一种更为安全高效的方式来实现单例,那就是枚举。
6. 单元素枚举实现单例模式
《Effective Java》中提到:
“单元素的枚举类型已经成为实现Singleton的最佳方法。”
枚举是一种定义了特殊约束规范的类,无法被继承,只能通过enum关键字来声明一个枚举,如通过单元素枚举实现的单例模式:
public enum Singleton6 {
INSTANCE;//后面没了,就这么多。
}
这其实是一种语法糖,反编译生成的class文件后的代码如下:
public final class Singleton6 extends Enum<Singleton6>{
public static final Singleton6 INSTANCE;
}
可以看出,枚举中的属性 INSTANCE 就相当于 Singleton6 的实例,当我们需要获取单例对象时,使用 Singleton6.INSTANCE 就可以了。当枚举 Singleton6 只有一个元素时,INSTANCE 就是其单例对象,而之前我们分析的单例模式需要解决的线程安全、反射、序列化和反序列化问题,现在都不用考虑,JVM 保证了每一个枚举类型和定义的枚举变量都是唯一的,枚举的定义本身就为单例模式的一些问题提供了很好的解决方案。关于 Enum 的源码可以自行查阅。
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
...
}
我们可以像常规类一样编写enum类,为其添加变量和方法,例如编写一个连接数据库的类:
public enum DateSourceEnum {
DATASOURCE;
private Connection conn = null;
private DateSourceEnum() {
try {
//加载数据库驱动
Class.forName("oracle.jdbc.driver.OracleDriver");
//获取数据库连接
conn = DriverManager.getConnection("jdbc:oracle:thin:@192.168.80.88:1521:orcl",
"root", "root");
} catch (Exception e) {
e.printStackTrace();
}
}
public Connection getConnection() {
return conn;
}
}
关于单例,我们应该记住:线程安全、延迟加载、序列化与反序列化安全、反射安全是关键,没有最完美的代码,只有最合适的应用方式。关于单例模式的总结就到这里了,欢迎指正~