单例模式
单例模式定义及应用场景
单例模式(Singleton Pattern)是指确定一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式是创建型模式。J2EE标准中的ServletContext、ServletContextConfig等、Spring 框架中应用的 ApplicationContext、数据库的连接池等也是单例形式。
饿汉式单例
饿汉式单例在类加载的时候就就 立刻初始化,并且创建单例对象。它线程绝对安全,在线程还没有出现以前就完成了实例化,不可能存在访问安全问题。
饿汉单例的标准写法:
/**
* 优点:执行效率高,性能高,没有任何的锁
* 缺点:某些情况下,可能会造成内存浪费
*/
public class HungrySingleton {
private static final HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton(){}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
还有另外一种写法,利用静态代码块的机制:
public class HungryStaticSingleton {
//先静态后动态
//先上,后下
//先属性后方法
private static final HungryStaticSingleton hungrySingleton;
//装个B
static {
hungrySingleton = new HungryStaticSingleton();
}
private HungryStaticSingleton(){}
public static HungryStaticSingleton getInstance(){
return hungrySingleton;
}
}
这两种写法都非常简单且容易理解。饿汉式单例适用于单例对象较少的情况,这样写可以保证绝对线程安全、执行效率比较高。但是它的缺点也比较明显,就是所有对象类加载的时候就会实例化。这样一来,如果系统中有大批量的单例对象存在,那系统初始化时就会导致大量的内存浪费。也就是说,不管对象用于不用都占着空间,浪费了内存。那有没有更优的写法呢,于是出现了懒汉式单例。
懒汉式单例模式
为了解决饿汉式单例可能带来的内存浪费问题,于是就出现了懒汉式单例的写法,懒汉式单例模式的特点是:单例对象要在被使用的时候才会被初始化。
/**
* 优点:节省了内存,线程安全
* 缺点:性能低
*/
public class LazySimpleSingletion {
private static LazySimpleSingletion instance;
private LazySimpleSingletion(){}
public synchronized static LazySimpleSingletion getInstance(){
if(instance == null){
instance = new LazySimpleSingletion();
}
return instance;
}
}
上面的代码,为了保证线程安全的问题,使用了synchronized关键词。但是,在synchronized加锁时,在线程数量比较多的情况下,如果 CPU分配压力上升,则会导致大批线程阻塞,从而导致程序性能大幅下降。那么有没有一种更好的方式,既能兼顾线程安全又能提升程序性能呢?答案肯定是有的,也就是我们经常见到的 DCL 双端检索单例模式,如下:
/**
* 优点:性能高了,线程安全了
* 缺点:可读性难度加大,不够优雅
*/
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();
//volatile 完美解决 指令重排序的问题
}
}
}
return instance;
}
}
使用 volatile 关键词,禁止指令重排,多线程下更安全。但是,用到 synchronize关键字总归要上锁,对程序性能还是有一定影响。难道就真的没有更好的解决方案 吗?答案肯定是有的,我们从类初始化的角度来考虑,采用静态内部类的方式:
/**
* 这种模式兼顾了饿汉式单例的内存浪费synchronize的性能问题,完美屏蔽了这两个缺点
*/
public class LazyStaticInnerClassSingleton {
//使用LazyStaticInnerClassSingleton的时候,默认会先初始化内部类,如果不使用,则内部类不会加载
private LazyStaticInnerClassSingleton() {
}
private static LazyStaticInnerClassSingleton getInstance() {
//在返回结果之前,一定会先加载内部类
return LazyHolder.INSTANCE;
}
//默认不加载
private static class LazyHolder {
private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
}
}
反射破坏单例
上面的单例模式构造方法中,只加了 private 关键字,没有做其他任何处理。如果我们使用反射来调用构造方法,再调用getInstance() 方法,应该有两个不同的实例,我们来进行测试破坏单例:
public class ReflectTest {
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);
}catch (Exception e){
e.printStackTrace();
}
}
}
运行结果如下:
显然创建了两个不同的实例,我们来进行一次代码优化:
public class LazyStaticInnerClassSingleton {
//使用LazyStaticInnerClassSingleton的时候,默认会先初始化内部类,如果不使用,则内部类不会加载
private LazyStaticInnerClassSingleton() {
if(LazyHolder.INSTANCE != null){
throw new RuntimeException("不允许非法访问");
}
}
private static LazyStaticInnerClassSingleton getInstance() {
//在返回结果之前,一定会先加载内部类
return LazyHolder.INSTANCE;
}
//默认不加载
private static class LazyHolder {
private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
}
}
优化后,再次运行:
发现已经不能被反射破坏了!至此,自认为史上最牛的单例模式的实现方式大功告成。但是,上面看似完美的单例写法还是有可能被破坏。
序列化破坏单例
一个单例对象创建好后,有时候需要将对象序列化后然后写入磁盘,下次使用的时候,然后再从磁盘中读取对象进行反序列化,将其转换为内存对象(此种情况只是根据个别场景使用,这里只是针针对破坏单例模式做讲解)。反序列化后的对象会重新分配内存,即重新创建。如果序列化的目标对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例。先来看一段代码:
public class SeriableSingleton implements Serializable {
/**
* 序列化
* 把内存中对象的状态转换为字节码的形式
* 把字节码通过IO输出流,写到磁盘上
* 永久保存下来,持久化
* <p>
* 反序列化
* 将持久化的字节码内容,通过IO输入流读到内存中来
* 转化成一个Java对象
*/
private final static SeriableSingleton INSTANCE = new SeriableSingleton();
private SeriableSingleton() {
}
public static SeriableSingleton getInstance() {
return INSTANCE;
}
}
下面来看测试代码,进行破坏单例模式:
public class SeriableSingletonTest {
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();
}
}
}
输出结果:
很明显,通过序列化和反序列化模式,已经破坏了单例模式。那么如何解决呢?来看下面的代码,进行优化后的:
public class SeriableSingleton implements Serializable {
/**
* 序列化
* 把内存中对象的状态转换为字节码的形式
* 把字节码通过IO输出流,写到磁盘上
* 永久保存下来,持久化
* <p>
* 反序列化
* 将持久化的字节码内容,通过IO输入流读到内存中来
* 转化成一个Java对象
*/
private final static SeriableSingleton INSTANCE = new SeriableSingleton();
private SeriableSingleton() {
}
public static SeriableSingleton getInstance() {
return INSTANCE;
}
/**
* 添加 readResolve 方法,解决序列化破坏单例模式
* @return
*/
private Object readResolve() {
return INSTANCE;
}
}
测试结果:
是不是很奇怪?为什么加入了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();
}
}
}
可以看出,方法里又调用了自身的readObject0()方法,继续跟进…
后面调试步骤不在逐一描述,调重点,来看readOrdinaryObject()方法,其中里面有一行判断是这样写的:
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
//通过反射进行创建对象
Object rep = desc.invokeReadResolve(obj);
//此处省略源码
...
}
重点逻辑为desc.hasReadResolveMethod(),来看源码是做了什么:
/**
* 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进行了赋值:
readResolveMethod = getInheritableMethod(
cl, "readResolve", null, Object.class);
上面的逻辑就是通过反射找到一个无参的readResolve方法,并且保存起来。现在回到ObjectInputStream的readOrdinaryObject()方法继续往下看,如果readResolve方法存在则调用
invokeReadResolve(),如下:
/**
* Invokes the readResolve method of the represented serializable class and
* returns the result. Throws UnsupportedOperationException if this class
* descriptor is not associated with a class, or if the class is
* non-serializable or does not define readResolve.
*/
Object invokeReadResolve(Object obj)
throws IOException, UnsupportedOperationException
{
requireInitialized();
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()方法返回实例解决了单例被破坏的问题。但是实际上实例化了两次,只不过新创建的对象没有被返回而已。如果创建对象的频率加快,就意味着内存分配开销也会随之增大。
注册式单例模式
注册式单例模式又被称为登记式单例模式,就是将每一个实例都登记到某一个地方,使用唯一的标识来获取实例。注册式单例模式有两种:
- 枚举式单例模式
- 容器式单例模式
枚举式单例模式
来看一下枚举式单例模式的写法:
public enum EnumSingleton {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumSingleton getInstance(){return INSTANCE;}
}
是不是很简单,其实就是简单的枚举。下面来测试:
public class EnumSingletonTest {
public static void main(String[] args) {
try {
Class clazz = EnumSingleton.class;
Constructor c = clazz.getDeclaredConstructor(String.class,int.class);
c.setAccessible(true);
System.out.println(c);
Object o = c.newInstance();
System.out.println(o);
}catch (Exception e){
e.printStackTrace();
}
}
}
运行结果:
可以看出,无法通过反射实例化,即反射无法破坏枚举模式的单例,那么再来看一下是否可以通过序列化来进行破坏:
public class SeriableSingletonTest {
public static void main(String[] args) {
EnumSingleton s1 = null;
EnumSingleton s2 = EnumSingleton.INSTANCE;
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 = (EnumSingleton) ois.readObject();
ois.close();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果:
依然完美,没有被破坏!
那这究竟是怎么回事呢?为何枚举式单例模式如此神奇?我们来通过分析源码解开它的面纱。
- 通过 jad 反编译工具,查看编译后的内容,可以发现枚举式单例模式在静态代码块中就给 INSTANCE 进行赋值,是饿汉式单例模式的实现;
- 通过反射时,发现找不到无参构造方法,及时添加了有参 的构造方法,JDK 也会提示不能通过反射来进行枚举的创建;
- 序列化时,在 readObject0() 方法中调用了 readEnum()方法,我们发现枚举类型其实是通过类名和类对象找到一个唯一的枚举对象,因此,枚举对象不可能被创建多次。
/**
* Reads in and returns enum constant, or null if enum type is
* unresolvable. Sets passHandle to enum constant's assigned handle.
*/
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;
}
容器式单例
其实枚举式单例,虽然写法优雅,但是也会有一些问题。因为它在类加载之时就将所有的对象都初始化放到内存中,这其实和饿汉式单例并无差异,不适合大量创建单例对象的场景。来看容器式单例模式的写法:
public class ContainerSingleton {
private ContainerSingleton() {
}
private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();
public static Object getInstance(String className) {
Object instance = null;
if (!ioc.containsKey(className)) {
try {
instance = Class.forName(className).newInstance();
ioc.put(className, instance);
} catch (Exception e) {
e.printStackTrace();
}
return instance;
} else {
return ioc.get(className);
}
}
}
容器式单例模式适用于需要大量创建单例对象的场景,便于管理。但它是非线程安全的。
单例模式在源码中的应用
JDK的一个经典应用,Runtime类:
public class Runtime {
private static Runtime currentRuntime = new Runtime();
/**
* Returns the runtime object associated with the current Java application.
* Most of the methods of class <code>Runtime</code> are instance
* methods and must be invoked with respect to the current runtime object.
*
* @return the <code>Runtime</code> object associated with the current
* Java application.
*/
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}
...
}
小结
本文讲了几种不同模式的单例,在实际应用过可以根据具体的业务场景进行选择使用。通过学习,加深对单例模式的理解,提高自己对单例模式的认识深度。
上一篇:工厂模式 https://blog.csdn.net/qq_20315217/article/details/114036722
下一篇:原型模式 https://blog.csdn.net/qq_20315217/article/details/114224897