单例模式
简单的说单例模式的作用就是保证整个应用程序的生命周期中,任何一时刻,单例类的实例都只存在一个。
饿汉式
public class Singleton {
/**
* 饿汉式
*/
private static Singleton singleton=new Singleton();
private Singleton (){}
public static Singleton getInstance(){
return singleton;
}
}
饿汉式单例本身是线程安全的,但它采用空间换取时间的方式,当类加载时马上就实例化Singleton对象,不管使用者用不用,后续每次调用 getInstance() 方法的时候,就不需要判断它是否实例化,从而节约了时间。但有些情况下需要懒加载实例化对象,针对这种情形,于是有了懒汉式的单例模式。
懒汉式
public class Singleton {
/**
* 懒汉式
*/
private static Singleton singleton;
private Singleton (){}
public static Singleton getInstance(){
if(singleton==null){
singleton=new Singleton();
}
return singleton;
}
}
这就是我们常见的懒汉式单例模式!这种懒汉式单例模式在多线程环境下,存在线程安全的问题!
双重检验锁
线程安全,延迟初始化。这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
public class Singleton {
/**
* 双重检验锁
*/
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getInstance(){
if(singleton==null){
synchronized (Singleton.class){
if(singleton==null){
singleton=new Singleton();
}
}
}
return singleton;
}
}
双重检查模式,进行了两次的判断,第一次是为了避免不要的实例,第二次是为了进行同步,避免多线程问题。由于singleton=new Singleton()对象的创建在JVM中可能会进行重排序,在多线程访问下存在风险,使用volatile修饰signleton实例变量有效,解决该问题。
- 双重检验锁也有问题,因为它无法解决反射对单例模式的破坏性。我将在静态内部类单例模式中加以阐述。
静态内部类
public class Singleton {
/**
* 静态内部类
*/
private Singleton (){}
private static final class SingletonHolder{
private static Singleton singleton=new Singleton();
}
private static Singleton getInstance(){
return SingletonHolder.singleton;
}
}
静态内部类
在了解静态内部类单例模式之前,让我们先了解一下静态内部类的两个知识。
- 静态内部类加载一个类时,其内部类不会同时被加载。
- 一个类被加载,当且仅当其某个静态成员如静态域、构造器、静态方法等被调用时才会被加载。
我们先看一个静态内部类的测试,以验证上面这两个观点。
package com.chloneda.jutils.test;
public class OuterClassTest {
private OuterClassTest() {}
static {
System.out.println("1、我是外部类静态模块...");
}
// 静态内部类
private static final class StaticInnerTest {
private static OuterClassTest oct = new OuterClassTest();
static {
System.out.println("2、我是静态内部类的静态模块... " + oct);
}
static void staticInnerMethod() {
System.out.println("3、静态内部类方法模块... " + oct);
}
}
public static void main(String[] args) {
OuterClassTest oct = new OuterClassTest(); // 此刻内部类不会被加载
System.out.println("===========分割线===========");
OuterClassTest.StaticInnerTest.staticInnerMethod(); // 调用内部类的静态方法
}
}
输出如下。
1、我是外部类静态模块...
=========分割线=========
2、我是静态内部类的静态模块... com.chloneda.jutils.test.OuterClassTest@b1bc7ed
3、静态内部类的方法模块... com.chloneda.jutils.test.OuterClassTest@b1bc7ed
从运行结果来看,验证是正确的!
由于静态内部类的特性,只有在其被第一次引用的时候才会被加载,所以可以保证其线程安全性。
由此可得出静态内部类单例模式的写法。
静态内部类
package com.chloneda.jutils.test;
/**
* 单例模式使用静态内部类方式实现,优点:实现代码简洁、延迟初始化、线程安全
*/
public class Singleton {
private static final class SingletonHolder {
private static Singleton INSTANCE = new Singleton();
}
private Singleton(){}
public static Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
}
这种写法的单例,外部无法访问静态内部类 SingletonHolder,只有当调用 Singleton.getInstance() 方法的时候,才能得到单例对象 INSTANCE。
而且静态内部类单例的 getInstance() 方法中没有使用 synchronized 关键字,提高了执行效率,同时兼顾了懒汉模式的内存优化(使用时才初始化,节约空间,达到懒加载的目的)以及饿汉模式的安全性。
但这种单例也有问题!这种方式需要两个类去做到这一点,也就是说,虽然懒加载静态内部类的对象,但其 外部类及内部静态类的 Class 对象还是会被创建,同时也无法防止反射对单例的破坏性(很多单例的写法都有这个通病),从而无法保证对象的唯一性。
我们通过以下测试类测试反射对静态内部类的破坏性。
/**
* @Description: 反射破坏静态内部类单例模式的测试类
*/
public class SingletonReflectTest {
public static void main(String[] args) {
//创建第一个实例
Singleton instance1 = Singleton.getInstance();
//通过反射创建第二个实例
Singleton instance2 = null;
try {
Class<Singleton> clazz = Singleton.class;
Constructor<Singleton> cons = clazz.getDeclaredConstructor();
cons.setAccessible(true);
instance2 = cons.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
//检查两个实例的hash值
System.out.println("Instance1 hashCode: " + instance1.hashCode());
System.out.println("Instance2 hashCode: " + instance2.hashCode());
}
}
输出结果如下。
Instance1 hashCode: 186370029
Instance2 hashCode: 2094548358
从输出结果可以看出,通过反射获取构造函数,并调用 setAccessible(true) 就可以调用私有的构造函数,从而得到Instance1和Instance2两个不同的对象。
静态内部类改进
如何防止这种反射对单例的破坏呢?我们可以通过修改构造器,让它在被要求创建第二个实例的时候抛出异常。
静态内部类修改如下。
/**
* @Description: 防止反射破坏静态内部类单例模式
*/
public class Singleton {
private static boolean initialized = false;
private static final class SingletonHolder {
private static Singleton INSTANCE = new Singleton();
}
private Singleton() {
synchronized (Singleton.class) {
if (initialized == false) {
initialized = !initialized;
} else {
throw new RuntimeException("单例模式禁止二次创建,防止反射!");
}
}
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
我们还用一个 SingletonReflectTest 测试类测试一下,输出结果如下。
java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.chloneda.jutils.test.SingletonReflectTest.main(Singleton.java:46)
Caused by: java.lang.RuntimeException: 单例模式禁止二次创建,防止反射!
at com.chloneda.jutils.test.Singleton.<init>(Singleton.java:24)
... 5 more
Instance1 hashCode: 1053782781
Exception in thread "main" java.lang.NullPointerException
at com.chloneda.jutils.test.SingletonReflectTest.main(Singleton.java:53)
所以我们通过修改构造器防止反射对单例的破坏性。
但是这种方式的单例也存在问题!什么问题呢?即序列化和反序列化之后无法继续保持单例(很多单例的写法也有这个通病)。
我们让上面防止反射破坏静态内部类的单例实现 Serializable 接口。
public class Singleton implements Serializable
并通过以下测试类进行序列化和反序列化测试。
/**
* @Description: 序列化破坏静态内部类单例模式的测试类
*/
pubic class SingletonSerializableTest {
public static void main(String[] args) {
try {
Singleton instance1 = Singleton.getInstance();
ObjectOutput out = null;
out = new ObjectOutputStream(new FileOutputStream("Singleton.ser"));
out.writeObject(instance1);
out.close();
//从文件中反序列化一个Singleton对象
ObjectInput in = new ObjectInputStream(new FileInputStream("Singleton.ser"));
Singleton instance2 = (Singleton) in.readObject();
in.close();
System.out.println("instance1 hashCode: " + instance1.hashCode());
System.out.println("instance2 hashCode: " + instance2.hashCode());
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出结果如下。
instance1 hashCode: 240650537
instance2 hashCode: 1566502717
从结果可以看出,很明显不是同一个单例对象!
那如何解决这个问题呢?
静态内部类再改进
我们可以实现 readResolve() 方法,它代替了从流中读取对象,确保了在序列化和反序列化的过程中没人可以创建新的实例。
可以得到改进版的静态内部类单例,可以有效防止序列化及反射的破坏!
package com.chloneda.jutils.test;
import java.io.*;
/**
* @Description: 可以防止序列化及反射破坏的静态内部类单例模式
*/
public class Singleton implements Serializable {
private static boolean initialized = false;
private static final class SingletonHolder {
private static Singleton INSTANCE = new Singleton();
}
private Singleton() {
synchronized (Singleton.class) {
if (initialized == false) {
initialized = !initialized;
} else {
throw new RuntimeException("单例模式禁止二次创建,防止反射!");
}
}
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
private Object readResolve() {
return getInstance();
}
}
我们再用上面的 SingletonSerializableTest 测试类测试一下结果。
输出结果如下。
instance1 hashCode: 240650537
instance2 hashCode: 240650537
此时就说明,单例在序列化和反序列化时的对象是一致的了。
其实上面饿汉式、懒汉式、双重校验锁及静态内部类单例所出现的问题,都可以通过枚举型单例进行解决,这也是《Effective Java》中推荐的写法。
枚举单例模式
public enum Singleton {
INSTANCE;
}
默认枚举实例的创建是线程安全的,并且在任何情况下都是单例。实际上
- 枚举类隐藏了私有的构造器。
- 枚举类的域 是相应类型的一个实例对象
那么枚举类型日常用例是这样子的:
public enum Singleton {
INSTANCE
//doSomething 该实例支持的行为
//可以省略此方法,通过Singleton.INSTANCE进行操作
public static Singleton get Instance() {
return Singleton.INSTANCE;
}
}
枚举单例模式在《Effective Java》中推荐的单例模式之一。但枚举实例在日常开发是很少使用的,就是很简单以导致可读性较差。
在以上所有的单例模式中,推荐静态内部类单例模式。主要是非常直观,即保证线程安全又保证唯一性。
众所周知,单例模式是创建型模式,都会新建一个实例。那么一个重要的问题就是反序列化。当实例被写入到文件到反序列化成实例时,我们需要重写readResolve
方法,以让实例唯一。
private Object readResolve() throws ObjectStreamException{
return singleton;
}
参考:
https://www.cnblogs.com/chloneda/p/pattern-singleton.html
https://www.jianshu.com/p/3bfd916f2bb2