单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
- 定义:保证一个类仅有一个实例,并提供一个全局访问点
- 类型:创建型
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
适用场景
优点与缺点
优点:
- 在内存里只有一个实例,减少内存开销
- 可以避免对资源的多重占用
- 设置全局访问点,严格控制访问
缺点:
单例模式的重点
- 私有构造函数,因为单例模式只提供一个全局访问点,不允许将构造函数暴露给用户
- 在多线程下,可能发生的线程安全问题
- 是否延迟加载,即大家熟知的
饿汉式
,懒汉式
- 序列化以及反序列化的安全问题
- 防止使用反射破坏单例
编码实现单例模式—懒汉式
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton() {
}
public synchronized static LazySingleton getInstance() {
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
在多线程下,为了避免多次创建实例,破坏单例。所以可以使用synchronized
来修饰方法,达到同步的目的。对于静态方法来说,锁住的是类的Class对象
,即其他线程无法再访问该类,Class对象的加锁与解锁需要消耗资源,且这段代码需要同步的只是下面代码。可以对这种方式进行改进。
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
双重检测懒汉式
public class LazyDoubleCheckSingleton {
private static LazyDoubleCheckSingleton lazySingleton = null;
private LazyDoubleCheckSingleton() {
}
public static LazyDoubleCheckSingleton getInstance() {
if (lazySingleton == null) {
synchronized (LazyDoubleCheckSingleton.class) {
if (lazySingleton == null) {
lazySingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazySingleton;
}
}
使用同步代码块,避免了只要访问getInstance
方法就锁住Class对象。双重判断也可以避免因为线程调度而产生的实例多次创建的问题。但这样也没完全避免线程安全问题,就是指令重排序带来的问题。这里不讨论指令重排序以及JMM和多线程相关的原理。
new LazyDoubleCheckSingleton()
其实包括了3个步骤:
- 分配对象的内存空间
- 初始化对象
- 设置instance指向内存可见(引用)
2和3是可以调换顺序的,由于2、3之间没有依赖关系,cpu可能会对指令重排序达到提高效率的目的。
所以可能出上图发生的情况,由于重排序,instance对象并没有完成初始化,但实际上此时instance对象已经不是null。把未初始化的对象发布了出去,这就产生了安全问题!
这里主要讲2种解决方法:禁用指令重排序以及对其他线程指令重排序不可见。
volatile禁用重排序
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton lazySingleton = null;
private LazyDoubleCheckSingleton() {
}
public static LazyDoubleCheckSingleton getInstance() {
if (lazySingleton == null) {
synchronized (LazyDoubleCheckSingleton.class) {
if (lazySingleton == null) {
lazySingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazySingleton;
}
}
volatile
禁用重排序的原理是内存屏障,这里不作讨论。
基于静态内部类的懒加载
public class StaticInnerClassSingleton {
private static class InnerClass{
private static final StaticInnerClassSingleton instance = new StaticInnerClassSingleton();
}
private StaticInnerClassSingleton() {
}
public static StaticInnerClassSingleton getInstance() {
return InnerClass.instance;
}
}
首先对于内部类,static final
修饰的常量,在类的初始化阶段就完成赋值了。并且类的初始化具有Class对象锁,只有一个线程可获取到Class对象锁。所以就算发生了重排序对于其他线程也是不可见的。从而屏蔽了重排序问题。
编码实现单例模式—饿汉式
public class HungrySingleton implements Serializable {
public static final HungrySingleton HUNGRY_SINGLETON = new HungrySingleton();
private HungrySingleton() {
if (HUNGRY_SINGLETON!=null) {
throw new RuntimeException("禁止在单例模式使用反射");
}
}
public static HungrySingleton getInstance() {
return HUNGRY_SINGLETON;
}
private Object readResolve() {
return HUNGRY_SINGLETON;
}
}
饿汉式是比较容易实现的,且没有线程安全问题。因为instance为常量,在类加载是就已经赋值了。
上面例子种增加了一些无关代码,下面再来解释一下原因,主要是针对反射破坏以及序列化破坏单例方面。
破坏单例模式的一些问题
主要包括:
- 在多线程下,可能发生的线程安全问题
- 序列化以及反序列化的安全问题
- 防止使用反射破坏单例
线程安全问题已经在上面说明了,就不赘述。
防止使用反射破坏单例
Class<HungrySingleton> objectClass = HungrySingleton.class;
Constructor<HungrySingleton> constructor = objectClass.getDeclaredConstructor();
constructor.setAccessible(true);
HungrySingleton newInstance = constructor.newInstance();
我们可以通过反射来调用单例类的私有构造器,从而破坏了单例模式。
private HungrySingleton() {
if (HUNGRY_SINGLETON!=null) {
throw new RuntimeException("禁止在单例模式使用反射");
}
}
这个时候,这段的代码的作用就看出来,由于在类加载阶段就已经生成了instance对象,我们在构造器对instance进行判断,抛出异常。
可以有效防止外部通过反射调用私有构造函数。但对于懒汉式,就很难防止反射攻击,因为懒汉式的instance对象是在getInstance
方法中生成的,所以无论采取什么方式,比如:标志位(flag),反射都可以获取到变量,修改变量来达到new
一个新的instance的目的。
序列化以及反序列化的安全问题
HungrySingleton instance = HungrySingleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("hungry_singleton"));
oos.writeObject(instance);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("hungry_singleton"));
HungrySingleton newInstance = (HungrySingleton) ois.readObject();
System.out.println(instance.getData() == newInstance.getData());
通过结果,输入输出的对象不是同一个对象。那么为什么两次的结果不一致,这里只展出jdk的关键源码。
obj = desc.isInstantiable() ? desc.newInstance() : null;
boolean isInstantiable() {
requireInitialized();
return (cons != null);
}
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
}
boolean hasReadResolveMethod() {
requireInitialized();
return (readResolveMethod != null);
}
readResolveMethod = getInheritableMethod(
cl, "readResolve", null, Object.class);
主要的逻辑就是先判断该类是不是可以支持序列化,然后先通过反射new一个instance,之后再判断类里是否有readResolve
方法,有则返回readResolve方法返回的object,没有则返回新new出的object。所以在单例类里补充readResolve
方法就可以解决序列化的问题。
private Object readResolve() {
return HUNGRY_SINGLETON;
}
单例模式最佳实践—基于枚举类
public enum EnumInstance {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumInstance getInstance() {
return INSTANCE;
}
}
我们使用jad来反编译这个类:
package com.qzh.design.pattern.creational.singleton;
public final class EnumInstance extends Enum
{
public static EnumInstance[] values()
{
return (EnumInstance[])$VALUES.clone();
}
public static EnumInstance valueOf(String name)
{
return (EnumInstance)Enum.valueOf(com/qzh/design/pattern/creational/singleton/EnumInstance, name);
}
private EnumInstance(String s, int i)
{
super(s, i);
}
public Object getData()
{
return data;
}
public void setData(Object data)
{
this.data = data;
}
public static EnumInstance getInstance()
{
return INSTANCE;
}
public static final EnumInstance INSTANCE;
private Object data;
private static final EnumInstance $VALUES[];
static
{
INSTANCE = new EnumInstance("INSTANCE", 0);
$VALUES = (new EnumInstance[] {
INSTANCE
});
}
}
可以看到INSTANCE
在类加载阶段就初始化了,所以是线程安全的。且具有一个私有的构造函数,符合单例模式。
并且枚举类是不支持反射的。在Constructor的newInstance
方法中
if ((this.clazz.getModifiers() & 16384) != 0) {
throw new IllegalArgumentException("Cannot reflectively create enum objects");
}
可以看到无法使用构造器来反射出枚举类对象。
枚举类也天生支持序列化,我们继续来看ObjectInputSteam
的代码:
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);
}
}
可以看到,是通过名称去获取枚举对线,而枚举对象又是static final
修饰的,所以拿到的都是同一个对象,有兴趣的朋友可以深入到Eunm的valueOf
方法去分析。
后记
总结了常见的单例模式的一些具体实现,以及一步步去优化代码,解决问题。以及解释大家平时不常用的用枚举类来实现单例。希望大家有所收获!
github地址
源码:https://github.com/DiangD/design_pattern_practice
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)