最近重新翻看了单例模式,梳理了一下单例模式创建的常用几种方法及优缺点。并思考了一些有关单例模式应用场景及其使用的必要性。
首先抛出单例模式的定义:
单例模式(Singleton Pattern):确保一个类有且只有
一个实例,并提供一个全局访问点。
单例使用场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数据库或文件的对象(比如数据源、Session工厂等)
在开发中,很多时候有一些对象其实我们只需要一个,例如:线程池(threadpool)
、缓存(cache)
、默认设置
、注册表(registry)
、日志对象
等等,这个时候把它设计为单例模式是最好的选择。
单例模式的好处:
它能避免实例对象的重复创建,不仅可以减少每次创建对象的时间开销,还可以节约内存空间(比如Spring管理的无状态bean);还能够避免由于操作多个实例导致的逻辑错误。如果一个对象有可能贯穿整个应用程序,而且起到了全局统一管理控制的作用,那么单例模式也许是一个值得考虑的选择。
这里放入一些自己的思考(单例模式使用的必要性):
我们都知道在Java中所有方法都是封装在类中的,代码的逻辑是在方法中。那可能就会想了,使用单例模式创建对象自然是要调用其相关的方法的,不然只创建一个占据内存空间的对象没有意义,那么为什么不直接使用类来代替单例模式呢?
通过查阅一些资料和大家的讨论后,借鉴后有一些自己的思考,这里大概总结一下(可能会有瑕疵或错误理解)。
1.代码灵活性角度。使用单例模式创建的是对象,对象就可继承,换句话说可以使用多态来完成程序的可扩展性。而直接使用类是做不到这一点的,因为类的静态方法不可重写。
2.内存使用角度。 静态方法跟静态成员变量一样,属于类的本身,在类装载的时候被装载到内存,不自动进行摧毁,直到JVM关闭(这里不太严谨,不过类卸载是很难的)。实例方法属于实例对象,实例化后才会分配内存,必须通过类的实例来引用,不会常驻内存,当实例对象被JVM回收之后,也跟着消失。
闲话不多说,先列举一下单例模式创建的几种方法:
1.饿汉式(静态变量)
2.饿汉式(静态代码块)
3.懒汉式(线程非安全)
4.懒汉式(线程安全、同步方法)
5.懒汉式(线程安全、同步代码块)跟上一种写法类似
6.懒汉式(线程安全、双重检验锁)推荐
7.静态内部类(线程安全)推荐
8.枚举(线程安全、反射安全、序列化/反序列化安全)推荐
单例模式的8种写法。
1、饿汉式(静态变量)
public class Singleton {
private final static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
这种基于classloader机制避免了多线程的同步问题,初始化的时候就给装载了。但是现在,没有懒加载的效果了。这是最简单的一种实现。如果确定对象一定会使用到,这种方法也不错。
2、饿汉式(静态代码块)
public class Singleton {
private static final Singleton instance ;
static {
instance = new Singleton();
}
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
跟上面差不多,变种写法,都是在类初始化时实例化instance
3.懒汉式(线程非安全)
public class Singleton {
private static Singleton instance;
private Singleton (){} //私有构造函数
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这种写法lazy loading(懒加载)很明显,但是一看就知道,存在线程安全问题,所以这种写法是被禁止的。
4.懒汉式(线程安全、同步方法)
public class Singleton {
private staticl Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
显然加了个synchronized来保证线程安全,but,效率太低了,毕竟99.99%的情况下是不需要同步的,有点用力过猛。极力不推荐使用。
5.懒汉式(线程安全、同步代码块)
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
synchronized(Singleton.class){
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
}
使用synchronized加代码块来保证同步,跟上一个类似。效率很低,不推荐。
6.懒汉式(线程安全、双重检验锁)
public class Singleton {
//这里volatile防止在对象创建的时候发生指令重排导致错误
// 1. 开辟空间 2.初始化对象数据 3.将指针指向开辟空间 步骤2和3有可能发生重排
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
// 注意此处还得有次判空~
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
使用到了volatile机制,俗称双重检查锁。既保证了效率,又保证了安全。代码稍微复杂点,但显得比较高级~
7.静态内部类(线程安全)
public class Singleton {
// 静态内部类
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
通过类加载器来保证对象创建的线程安全和懒加载。这种方式Singleton类
被装载了,instance
不会被立马初始化,因为SingletonHolder类
没有被主动使用,只有显示通过调用getInstance
方法时,才会显示装载SingletonHolder类,显然它达到了lazy loading效果。推荐使用。
8.枚举
public enum Singleton {
INSTANCE;
}
使用枚举方式实现。
这种方式是Effective Java
作者Josh Bloch
提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,可谓是很坚强的壁垒啊。 所以这种写法,是十分推荐的且是最优的。
为何枚举方式
是最好的单例实现方式?
前几种方式实现单例都有如下3个特点:
这种实现方式的问题就在低一点:私有化构造器并不保险
。因为它抵御不了反射攻击
,比如如下示例代码:
下面实验 饿汉式 被 反射攻击:
public class Singleton implements Serializable {
private static final Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
public class Main {
public static void main(String[] args) throws Exception {
Singleton s = Singleton.getInstance();
// 拿到所有的构造函数,包括非public的
Constructor constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
// 使用空构造函数new一个实例。即使它是private的~~~
Singleton sReflection = constructor.newInstance();
System.out.println(s); //com.wzy.bean.Singleton@1f32e575
System.out.println(sReflection); //com.wzy.bean.Singleton@279f2327
System.out.println(s == sReflection); // false
}
}
运行输出:
com.wzy.bean.Singleton@1f32e575
com.wzy.bean.Singleton@279f2327
false
通过反射,竟然给所谓的单例创建出了一个新的实例对象。所以这种方式也还是存在不安全因素的。怎么破???如何解决??? 其实Joshua Bloch
说了:可以在构造函数在被第二次调用的时候抛出异常。具体示例代码,可以参考枚举实现的源码。
再看看它的序列化、反序列时会不会有问题。如下:
注意:JDK的序列化、反序列化底层并不是反射~~~
public class Main {
public static void main(String[] args) throws Exception {
Singleton s = Singleton.getInstance();
byte[] serialize = SerializationUtils.serialize(s);
Object deserialize = SerializationUtils.deserialize(serialize);
System.out.println(s);
System.out.println(deserialize);
System.out.println(s == deserialize);
}
}
运行结果:
com.wzy.bean.Singleton@452b3a41
com.wzy.bean.Singleton@6193b845
false
可以看出,序列化前后两个对象并不相等。所以它序列化也是不安全的
下面看看枚举大法
使用枚举实现单例极其的简单:
首先看看是否防御 反射攻击:
public enum EnumSingleton {
INSTANCE;
}
public class Main {
public static void main(String[] args) throws Exception {
EnumSingleton s = EnumSingleton.INSTANCE;
// 拿到所有的构造函数,包括非public的
Constructor constructor = EnumSingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
// 使用空构造函数new一个实例。即使它是private的~~~
EnumSingleton sReflection = constructor.newInstance();
System.out.println(s);
System.out.println(sReflection);
System.out.println(s == sReflection); // false
}
}
结果运行就报错:
Exception in thread "main" java.lang.NoSuchMethodException: com.wzy.bean.EnumSingleton.()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at com.wzy.maintest.Main.main(Main.java:19)
这个看起来是因为没有空的构造函数导致的,还并不能下定义说防御了反射攻击。那它有什么构造函数呢,可以看它的父类Enum类:
// @since 1.5 它是所有Enum类的父类,是个抽象类
public abstract class Enum> implements Comparable, Serializable {
// 这是它的唯一构造函数,接收两个参数(若没有自己额外指定构造函数的话~)
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
...
}
这里我们可以通过反编译看下 枚举类:javap -p EnumSingleton.class
$ javap -p EnumSingleton.class
Compiled from "EnumSingleton.java"
public final class com.wzy.bean.EnumSingleton extends java.lang.Enum {
public static final com.wzy.bean.EnumSingleton INSTANCE;
private static final com.wzy.bean.EnumSingleton[] $VALUES;
public static com.wzy.bean.EnumSingleton[] values();
public static com.wzy.bean.EnumSingleton valueOf(java.lang.String);
private com.wzy.bean.EnumSingleton();
static {};
}
可以看到其继承java.lang.Enum类,且是final修饰不可修改的。
可以看它的父类Enum类,既然它有这个构造函数,那我们就先拿到这个构造函数再创建对象试试:
public class Main {
public static void main(String[] args) throws Exception {
EnumSingleton s = EnumSingleton.INSTANCE;
// 拿到所有的构造函数,包括非public的
Constructor constructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);// 拿到有参的构造器
constructor.setAccessible(true);
// 使用空构造函数new一个实例。即使它是private的~~~
System.out.println("拿到了构造器:" + constructor);
EnumSingleton sReflection = constructor.newInstance("testInstance", 1);
System.out.println(s);
System.out.println(sReflection);
System.out.println(s == sReflection); // false
}
}
运行打印:
拿到了构造器:private com.wzy.bean.EnumSingleton(java.lang.String,int)
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at com.wzy.maintest.Main.main(Main.java:22)
第一句输出了,表示我们是成功拿到了构造器Constructor
对象的,只是在执行newInstance
时候报错了。并且也提示报错在Constructor
的417行,看看Constructor
的源码处:
public final class Constructor extends Executable {
...
public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
...
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
...
}
...
}
主要是这一句:(clazz.getModifiers() & Modifier.ENUM) != 0
。说明:反射在通过newInstance创建对象时,会检查该类**是否ENUM修饰**,如果是则抛出异常,反射失败
,因此枚举类型对反射是绝对安全的。
那么,枚举对序列化、反序列化是否安全?
public class Main {
public static void main(String[] args) {
EnumSingleton s = EnumSingleton.INSTANCE;
byte[] serialize = SerializationUtils.serialize(s);
Object deserialize = SerializationUtils.deserialize(serialize);
System.out.println(s == deserialize); //true
}
}
结果是:true
。因此:枚举类型对序列化、反序列也是安全的。
综上,可以得出结论:枚举是实现单例模式的最佳实践。毕竟使用它全都是优点:
单例模式在JDK应用的源码
java.lang.Runtime就是经典的单例模式(饿汉式)