1 单例模式的应用场景
单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式是创建型模式。单例模式在现实生活中应用也非常广泛。例如, 国家主席、公司CEO 、部门经理等。在J2EE标准中,ServletContext、ServletContextConfig 等;在Spring 框架应用中ApplicationContext;数据库的连接池也都是单例形式。
2 基础结构
单例模式的基础结构就三点:
- 全局实例引用instance。一个静态的引用,用来存放全局唯一的对象引用;
- 私有构造方法Singleton()。私有构造方法的目的是为了防止有人通过构造方法new新的对象,破坏单例;
- 暴露一个获取实例的方法getInstance()。为了保证单例的安全性,通常会所有外部能修改对象引用的方式都屏蔽掉,提供一个获取实例的方法对外调用就行了。
3 多种方式介绍
单例模式的实现方式有多种,各有利弊。比较常见的是饿汉式单例和懒汉式单例。
3.1 饿汉式单例
public class HungrySingleton {
private static final HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton(){}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
饿汉式单例是在类加载的时候就立即初始化,并且创建单例对象。绝对线程安全,在线程还没出现以前就是实例化了,不可能存在访问安全问题。
优点:没有加任何的锁、执行效率比较高,在用户体验上来说,比懒汉式更好。
缺点:类加载的时候就初始化,不管用与不用都占着空间,浪费了内存,有可能占着茅坑不拉屎。
3.2 懒汉式单例
3.2.1 懒汉式的简单实现
public class LazySimpleSingleton {
private static LazySimpleSingleton lazy = null;
private LazySimpleSingleton() {
}
public static LazySimpleSingleton getInstance() {
if (lazy == null) {
lazy = new LazySimpleSingleton();
}
return lazy;
}
}
写一个线程类ExectorThread 类
public class ExectorThread implements Runnable {
@Override
public void run() {
LazySimpleSingleton singleton = LazySimpleSingleton.getInstance();
System.out.println(Thread.currentThread().getName() + ":" + singleton);
}
}
测试
public class SingletonTest {
public static void main(String[] args) {
Thread thread1 = new Thread(new ExectorThread());
Thread thread2 = new Thread(new ExectorThread());
thread1.start();
thread2.start();
System.out.println("end");
}
}
输出
end
Thread-0:com.hsf.study.homework.designpatterns.singleton.lazy.LazySimpleSingleton@275635de
Thread-1:com.hsf.study.homework.designpatterns.singleton.lazy.LazySimpleSingleton@199f4eb8
一定几率出现创建两个不同结果的情况,意味着上面的单例存在线程安全隐患。为了解决这个问题,我们可以考虑加锁。
3.2.2 双重检查锁实现单例
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton lazy = null;
private LazyDoubleCheckSingleton(){}
public static LazyDoubleCheckSingleton getInstance(){
if(lazy == null){
synchronized (LazyDoubleCheckSingleton.class){
if(lazy == null){
lazy = new LazyDoubleCheckSingleton();
}
}
}
return lazy;
}
}
在获取实例时会先判空,如果为空才会去拿锁,然后再次判空,为空便创建一个新对象。这么做的目的在于保证单例的同时,尽量少的出现并发抢锁引起大量阻塞的情况发生,导致程序性能大幅度下降。
值得注意的是这里对lazy实例加了volatile关键字修饰,要理解为什么要加volatile,首先要理解new
LazyDoubleCheckSingleton()
做了什么。new一个对象有几个步骤:1.看class对象是否加载,如果没有就先加载class对象,2.分配内存空间,初始化实例,3.调用构造函数,4.返回地址给引用。而cpu为了优化程序,可能会进行指令重排序,打乱这3,4这几个步骤,导致实例内存还没分配,就被使用了。所以假设线程A拿到了锁去创建实例,这时候指令重排,先返回了引用地址,但是还没有调用构造函数,此时cpu切换,到了线程B获取实例,发现引用不为空,直接返回引用,接着使用的时候会发现实例并没有初始化,于是报错。使用volatile关键字修饰之后,jvm就不会对lazy实例的new操作进行指令重排。
3.2.3 内部类实现单例
上面使用双重检查锁实现单例已经很完美了,但是,用到synchronized 关键字,总归是要上锁,对程序性能还是存在一定影响的。难道就真的没有更好的方案吗?当然是有的。我们可以从类初始化角度来考虑,看下面的代码,采用静态内部类的方式:
public class LazyInnerClassSingleton {
private LazyInnerClassSingleton(){}
/**
* 每一个关键字都不是多余的
* static 是为了使单例的空间共享
* final 保证这个方法不会被重写,重载
*/
public static final LazyInnerClassSingleton getInstance(){
//在返回结果以前,一定会先加载内部类
return LazyHolder.LAZY;
}
/**
* 默认不加载内部类
*/
private static class LazyHolder{
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}
这种形式兼顾饿汉式的内存浪费,也兼顾synchronized 性能问题。内部类一定是要在方法调用之前初始化,巧妙地避免了线程安全问题。
我们可以看一下时序图:
1、客户端调用LazyInnerClassSingleton.getInstance(),此时会先判断LazyInnerClassSingleton这个类是否已经加载,如果没有加载则先加载,然后调用getInstance方法;
2、getInstance方法内调用了LazyHolder.LAZY,则此时会先判断LazyHolder这个类是否已经加载,如果没有加载则先加载,并初始化自身的静态属性,此时LAZY通过new LazyInnerClassSingleton()完成了初始化;
3、返回LazyHolder的属性LAZY的引用,最终把引用返回到客户端;
从上面的流程逻辑,我们可以看到,内部类是在方法调用之前初始化,如果在getInstance方法中没有调用LazyHolder.LAZY,那么LazyHolder是不会完成初始化的,巧妙地避免了线程安全问题,同时节省了系统的开销。
3.3 注册式单例
注册式单例又称为登记式单例,就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例。注册式单例有两种写法:一种为容器缓存,一种为枚举登记。
3.3.1 枚举登记
创建枚举类EnumSingleton
public enum EnumSingleton {
INSTANCE;
public static EnumSingleton getInstance(){
return INSTANCE;
}
}
就这样,很简单的就实现了一个单例。这是依赖了枚举类的一些特性,因为每个枚举元素就是一个实例,所以这里如果写了两个以上的枚举元素,就不能叫单例了,哈哈。
3.3.2 容器缓存
创建ContainerSingleton 类
public class ContainerSingleton {
private ContainerSingleton(){}
private static Map<String,Object> ioc = new ConcurrentHashMap<String,Object>();
public static Object getInstance(String className){
synchronized (ioc) {
if (!ioc.containsKey(className)) {
Object obj = null;
try {
obj = Class.forName(className).newInstance();
ioc.put(className, obj);
} catch (Exception e) {
e.printStackTrace();
}
return obj;
} else {
return ioc.get(className);
}
}
}
}
容器式写法适用于创建实例非常多的情况,便于集中管理。有局限性,如果客户越过容器,自己创建实例,则会破坏单例性。
我们还可以来看看容器式单例在Spring中使用的一个例子:
public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory
implements AutowireCapableBeanFactory {
/** Cache of unfinished FactoryBean instances: FactoryBean name --> BeanWrapper */
private final Map<String, BeanWrapper> factoryBeanInstanceCache = new ConcurrentHashMap<>(16);
...
}
4 完善
基本上,单例模式就上面的几种实现方式了。那么上面的实现是不是就完美了呢?好的代码是要经得起考验的,单例模式的核心里面就体现在“单例”两个字上,那么我们写的单例会被破坏吗?
要破坏单例,首先我们要知道创建一个新对象都有哪些方式:new、反射、克隆、反序列化。
4.1 针对new
普通class类,外部new对象的方式创建新实例,使用private关键字修饰构造方法即可;
枚举类,无法通过构造方法创建新的实例,它的实例都是事先定义好的枚举元素,天然免疫这个问题。
4.2 针对反射
以内部类单例为例,通过反射创建新的实例
public static void main(String[] args) {
try {
Class<?> clazz = LazyInnerClassSingleton.class;
Constructor c = clazz.getDeclaredConstructor(null);
c.setAccessible(true);
Object o1 = c.newInstance();
Object o2 = c.newInstance();
System.out.println(o1 == o2);
} catch (Exception e) {
e.printStackTrace();
}
}
输出结果为false。
从结果上看,我们的单例是被破坏了,那有没有什么办法可以避免这个问题呢?既然是通过反射执行构造方法创建的实例,那么我们可以在构造方法里面加点料
private LazyInnerClassSingleton(){
if(LazyHolder.LAZY != null){
throw new RuntimeException("不允许创建多个实例");
}
}
再次运行得到以下结果:
上面时候普通class类的处理方式,那么枚举式单例呢?
public static void main(String[] args) {
try {
Class<?> clazz = EnumSingleton.class;
Constructor c = clazz.getDeclaredConstructor(null);
c.setAccessible(true);
Object o1 = c.newInstance();
Object o2 = c.newInstance();
System.out.println(o1 == o2);
} catch (Exception e) {
e.printStackTrace();
}
}
输出结果如下:
说的是没有找到构造方法,那我们看一下java.lang.Enum的源码,查看它的构造方法,只有一个protected的构造方法,代码如下:
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
然后再来做这样一个测试:
public static void main(String[] args) {
try {
Class clazz = EnumSingleton.class;
Constructor c = clazz.getDeclaredConstructor(String.class,int.class);
c.setAccessible(true);
EnumSingleton enumSingleton = (EnumSingleton)c.newInstance("hsf",666);
}catch (Exception e){
e.printStackTrace();
}
}
输出结果如下:
结果里面说的很明白了,Cannot reflectively create enum objects,不能通过发射创建枚举对象!
来看看JDK 源码,进入Constructor的newInstance()方法:
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
在newInstance()方法中做了强制性的判断,如果修饰符是Modifier.ENUM 枚举类型,直接抛出异常。到这为止,我们是不是已经非常清晰明了呢?枚举式单例也是《Effective Java》书中推荐的一种单例实现写法。在JDK 枚举的语法特殊性,以及反射也为枚举保驾护航,让枚举式单例成为一种比较优雅的实现。
4.3 针对克隆
我们来看一下java.lang.Object里的clone()方法:
protected native Object clone() throws CloneNotSupportedException;
它的修饰符是protected,意味着本类及子类可以调用,那么我们让LazyInnerClassSingleton类实现Cloneable,并重写clone方法,将protected关键字改成public再来测试:
public static void main(String[] args) throws CloneNotSupportedException {
LazyInnerClassSingleton instance = LazyInnerClassSingleton.getInstance();
System.out.println(instance);
System.out.println(instance.clone());
}
输出结果如下:
那么为了避免出现这种情况,我们应该避免暴露相关的克隆方法出去,当然,既然是单例,基本是不会需要克隆这种操作的,所以了解一下就行了。
4.4 针对序列化
当我们将一个单例对象创建好,有时候需要将对象序列化然后写入到磁盘,下次使用时再从磁盘中读取到对象,反序列化转化为内存对象。反序列化后的对象会重新分配内存,即重新创建。那如果序列化的目标的对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例,来看一段代码(在这之前,要先让LazyInnerClassSingleton实现Serializable接口):
public static void main(String[] args) {
LazyInnerClassSingleton s1 = null;
LazyInnerClassSingleton s2 = LazyInnerClassSingleton.getInstance();
FileOutputStream fos = null;
try {
fos = new FileOutputStream("LazyInnerClassSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("LazyInnerClassSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (LazyInnerClassSingleton) ois.readObject();
ois.close();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
} catch (Exception e) {
e.printStackTrace();
}
}
输出结果如下:
运行结果中,可以看出,反序列化后的对象和手动创建的对象是不一致的,实例化了两次,违背了单例的设计初衷。那么,我们如何保证序列化的情况下也能够实现单例?其实很简单,只需要增加readResolve()方法即可。来看优化代码:
public class LazyInnerClassSingleton implements Serializable{
private LazyInnerClassSingleton() {
if (LazyHolder.LAZY != null) {
throw new RuntimeException("不允许创建多个实例");
}
}
/**
* 每一个关键字都不是多余的
* static 是为了使单例的空间共享
* final 保证这个方法不会被重写,重载
*/
public static final LazyInnerClassSingleton getInstance(){
//在返回结果以前,一定会先加载内部类
return LazyHolder.LAZY;
}
/**
* 默认不加载内部类
*/
private static class LazyHolder{
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
private Object readResolve(){
return LazyHolder.LAZY;
}
}
再看运行结果:
咦?为什么会这样子呢?只是加了一个readResolve()方法,好像也没做什么特殊的操作啊?那我们一起来看看JDK的源码实现就明白了。我们进入ObjectInputStream 类的readObject()方法,代码如下:
public final Object readObject()
throws IOException, ClassNotFoundException
{
if (enableOverride) {
return readObjectOverride();
}
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(false);
handles.markDependency(outerHandle, passHandle);
ClassNotFoundException ex = handles.lookupException(passHandle);
if (ex != null) {
throw ex;
}
if (depth == 0) {
vlist.doCallbacks();
}
return obj;
} finally {
passHandle = outerHandle;
if (closed && depth == 0) {
clear();
}
}
}
我们发现在readObject 中又调用了我们重写的readObject0()方法。进入readObject0()方法,代码如下:
private Object readObject0(boolean unshared) throws IOException {
...
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
...
}
我们看到TC_OBJECTD 中判断,调用了ObjectInputStream 的readOrdinaryObject()方法,我们继续进入看源码:
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);
}
...
return obj;
}
发现调用了ObjectStreamClass 的isInstantiable()方法,而isInstantiable()里面的代码如下:
boolean isInstantiable() {
requireInitialized();
return (cons != null);
}
代码非常简单,就是判断一下构造方法是否为空,构造方法不为空就返回true。意味着,只要有无参构造方法就会实例化。
这时候,其实还没有找到为什么加上readResolve()方法就避免了单例被破坏的真正原因。我再回到ObjectInputStream 的readOrdinaryObject()方法继续往下看:
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) {
handles.setObject(passHandle, obj = rep);
}
}
return obj;
}
判断无参构造方法是否存在之后,又调用了hasReadResolveMethod()方法,来看代码:
boolean hasReadResolveMethod() {
requireInitialized();
return (readResolveMethod != null);
}
逻辑非常简单,就是判断readResolveMethod 是否为空,不为空就返回true。那么readResolveMethod 是在哪里赋值的呢?通过全局查找找到了赋值代码在私有方法ObjectStreamClass()方法中给readResolveMethod 进行赋值,来看代码:
readResolveMethod = getInheritableMethod(cl, "readResolve", null, Object.class);
上面的逻辑其实就是通过反射找到一个无参的readResolve()方法,并且保存下来。现在再回到ObjectInputStream 的readOrdinaryObject() 方法继续往下看, 如果readResolve()存在则调用invokeReadResolve()方法,来看代码:
Object invokeReadResolve(Object obj)
throws IOException, UnsupportedOperationException
{
if (readResolveMethod != null) {
try {
return readResolveMethod.invoke(obj, (Object[]) null);
} catch (InvocationTargetException ex) {
Throwable th = ex.getTargetException();
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()方法返回实例,解决了单例被破坏的问题。但是,我们通过分析源码以及调试,我们可以看到实际上实例化了两次,只不过新创建的对象没有被返回而已。那如果,创建对象的动作发生频率增大,就意味着内存分配开销也就随之增大。
以上是普通class类对反序列化破坏单例的处理分析。那么反序列化破坏枚举式单例又会怎样呢?我们不妨再来看一下JDK源码,还是回到ObjectInputStream 的readObject0()方法:
private Object readObject0(boolean unshared) throws IOException {
...
case TC_ENUM:
return checkResolve(readEnum(unshared));
...
}
我们看到在readObject0()中调用了readEnum()方法,来看readEnum()中代码实现:
private Enum<?> readEnum(boolean unshared) throws IOException {
if (bin.readByte() != TC_ENUM) {
throw new InternalError();
}
ObjectStreamClass desc = readClassDesc(false);
if (!desc.isEnum()) {
throw new InvalidClassException("non-enum class: " + desc);
}
int enumHandle = handles.assign(unshared ? unsharedMarker : null);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
handles.markException(enumHandle, resolveEx);
}
String name = readString(false);
Enum<?> result = null;
Class<?> cl = desc.forClass();
if (cl != null) {
try {
@SuppressWarnings("unchecked")
Enum<?> en = Enum.valueOf((Class)cl, name);
result = en;
} catch (IllegalArgumentException ex) {
throw (IOException) new InvalidObjectException(
"enum constant " + name + " does not exist in " +
cl).initCause(ex);
}
if (!unshared) {
handles.setObject(enumHandle, result);
}
}
handles.finish(enumHandle);
passHandle = enumHandle;
return result;
}
我们发现枚举类型其实通过类名和Class 对象类找到一个唯一的枚举对象。因此,枚举对象不可能被类加载器加载多次。
5 总结
类型 | 优点 | 缺点 |
---|---|---|
饿汉式单例 | 1.简单,易于理解; 2.安全,不会存在线程问题。 | 1.过早的创建实例,可能会存在“占着茅坑不拉屎”的情况,浪费系统资源。 |
双重检查锁单例 | 1.不会过早的过早的创建实例,节约系统资源。 | 1.存在同步锁,如果发生大量阻塞,会造成系统性能下降; 2.相对饿汉式变得更复杂。 |
内部类单例 | 1.不会过早的过早的创建实例,节约系统资源; 2.不存在同步锁,只有在内部类被调用的时候才会创建对象。 | 1.通过增加readResolve()方法来避免发序列化被破坏时,还是会再次创建一个新的实例,一定程度上的会浪费系统资源。 |
枚举登记单例 | 1.实现简单; 2.JDK的宠儿,天生受JDK保护,不会被反射、反序列化等方式破坏单例。 | 1.要求类必须是枚举类; 2.枚举元素必须有且只有一个。 |
容器式单例 | 1.集中管理单例,可以更自由、更个性化的管理单例。 | 1.如果客户越过容器,自己创建实例,则会破坏单例性。 |