设计模式_单例模式
一、定义
单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个示例,并提供一个全局访问点。单例模式属于创建型模式
二、应用场景
现实生活中的如:公司CEO、部门经理等。 J2EE标准中的如:ServletContext、ServletContextConfig等、Spring框架应用中的ApplicationContext、数据库的连接池等,这些都是单例模式
三、单例模式的常见写法
1、饿汉式单例
public class HungrySingleton {
//private 对外不提供
//static 类加载时已经获取对象
//final 不管通过哪种方式 如反射等 不允许有多个值,保证单例的唯一性
//此处也可以使用静态代码块方式 实现饿汉式单例模式
private static final HungrySingleton HUNGRY_SINGLETON = new HungrySingleton();
//私有化
private HungrySingleton(){};
public static HungrySingleton getInstance(){
return HUNGRY_SINGLETON;
}
}
优点: 线程安全,执行效率高。
缺点: 由于类加载时就初始化,不管用与不用,都会占用空间,浪费内存,是一种以时间换区空间的效果。
注意点: 如果项目少量这些类可以使用,但是如果较多,则不建议,如Spring容器就没有采用这种方式。
2、懒汉式(同步锁)
public class LazySimpleSingleton {
private static LazySimpleSingleton lazySimpleSingleton = null;
private LazySimpleSingleton() {
}
//synchronized 具有性能问题
public synchronized static LazySimpleSingleton getInstance() {
if (lazySimpleSingleton == null)
lazySimpleSingleton = new LazySimpleSingleton();
return lazySimpleSingleton;
}
}
优点: 需要时再初始化,节省资源。线程安全。
缺点: 需要加锁synchronized(而且锁作用域较高,属于类锁),影响效率。
注意点: 这里代码加了synchronized,如果没有加,则会有线程不安全的情况,如:多线程情况获取的类的实例是不相等的。
3、懒汉式(DCL)
public class LazyDoubleCheckSingleton {
//双重检查锁 会牵扯CPU层面的指令重排序 所以需要volatile
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton() {
}
//较之LazySimpleSingleton 适中的方法
public static LazyDoubleCheckSingleton getInstance() {
//第一次 判null 减少锁的竞争 检查是否要阻塞
if (lazyDoubleCheckSingleton == null)
//synchronized的作用域区别 这里是代码块 LazySimpleSingleton是类锁
synchronized (LazyDoubleCheckSingleton.class){
//第二次 判null 是否要创建实例
if(lazyDoubleCheckSingleton == null)
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
return lazyDoubleCheckSingleton;
}
}
优点: 经过双重判null处理以及synchronized作用域降级(由类锁变为代码块锁),提升性能。含有懒汉式的优点。
缺点: 代码复杂,切记需要通过validate关键字修饰lazyDoubleCheckSingleton变量,达到禁止指令重排序的效果。
注意点:
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
这段代码,其实在JVM层面已经转换为多条指令。
- 指令1 分配对象的内存空间
- 指令2 初始化对象
- 指令3
设置
lazyDoubleCheckSingleton
指向刚分配的内存地址
经过重排序后如下:
-
指令1 分配对象的内存空间
-
指令2 设置
lazyDoubleCheckSingleton
指向刚分配的内存地址,此时对象还没被初始化 -
指令3 初始化对象 两次判null 意义不同,第一次 判null 减少锁的竞争 检查是否要阻塞,第二次 判null 是否要创建实例。
4、懒汉式(静态内部类)
public class LazyInnerClassSingleton { //防止创建多次,如:反射破坏单例时 private LazyInnerClassSingleton() { if (LazyInnerInnerHungry.LAZY != null) { throw new RuntimeException("不允许创建多个实例"); } } //外部调用时才执行 属于懒汉式 public static final LazyInnerClassSingleton getInstance() { return LazyInnerInnerHungry.LAZY; } //属于饿汉式 默认不加载 //内部类一定是要在方法调用之前初始化,巧妙地避免了线程安全问题 private static class LazyInnerInnerHungry { private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton(); } }
优点: 既没有synchronized关键字带来的效率问题,也是一种懒加载机制的单例模式,节省资源,用时再初始化。
缺点: 能够被反射破坏(私有化构造方法添加判断后无该问题),代码看起来怪怪的,不优雅。
注意点:
LazyInnerClassSingleton.getInstance
方法在调用时才去执行,是懒汉式的。
因为类加载机制,LazyInnerClassSingleton.class执行getInstance方法时才去加载LazyInnerClassSingleton$LazyInnerInnerHungry.class的创建实例的方法。
因为可以通过反射破坏,为了解决该问题,在私有化构造方法内,添加判断是否已经创建实例,若创建则抛异常,这样可解决反射破坏问题。
5、注册式单例模式-枚举登记
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;
}
}
优点: 代码简洁、简单,线程安全,从JDK层面已经避免了反射或序列化和反序列化破坏。从代码层面是一种最优雅的单例模式。
缺点: 因为也属于饿汉式单例,故有饿汉式单例的问题,即单例创建时机不可控制。
6、注册式单例模式-容器缓存
public class ContainerSingleton {
//容器
private static Map<String,Object> ioc = new ConcurrentHashMap<>();
private ContainerSingleton(){
}
public static Object getBean(String className){
//类不在该容器中 通过反射找到该类,并且放到容器中
if(!ioc.containsKey(className)){
synchronized (ContainerSingleton.class){
if(!ioc.containsKey(className)) {
Object o = null;
try {
o = Class.forName(className).newInstance();
ioc.put(className, o);
} catch (Exception e) {
e.printStackTrace();
}
return o;
}else{
//容器中含有该类,直接返回。
return ioc.get(className);
}
}
}else {
//容器中含有该类,直接返回。
return ioc.get(className);
}
}
}
优点: 当实例对象过多时,为了方便统一管理,可使用容器缓存方式管理。懒加载的方式
缺点: 线程不安全。需要加锁。
7、伪单例模式
public class ThreadLocalSingleton {
private ThreadLocalSingleton(){
}
private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance = new ThreadLocal<ThreadLocalSingleton>(){
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
public static ThreadLocalSingleton getInstance(){
return threadLocalInstance.get();
}
}
优点: 天生线程安全
缺点: 唯一性的作用域局限性,仅仅局限于同一线程。
四、破坏案例
1、反射
1.1 比如对懒汉式(静态内部类)案例进行反射破坏
public class LazyInnerClassSingletonTest {
public static void main(String[] args) {
try {
//通过反射破坏
Class<?> clazz = LazyInnerClassSingleton.class;
//通过反射拿到私有的构造方法
Constructor constructor = clazz.getDeclaredConstructor();
//赋予访问权限,强制访问
constructor.setAccessible(true);
//暴力初始化
Object o1 = constructor.newInstance();
//再次初始化
Object o2 = constructor.newInstance();
System.out.println(o1);
System.out.println(o2);
System.out.println(o1 == o2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
结论:当使用反射破坏懒汉式(静态内部类)时,需要在构造方法中判断是否已经初始化,来解决反射破坏
1.2 比如对注册式单例模式-枚举登记案列进行反射破坏
public class EnumSingletonTest {
public static void main(String[] args) {
try {
Class clazz = EnumSingleton.class;
Constructor c = clazz.getDeclaredConstructor();
c.setAccessible(true);
Object o = c.newInstance();
System.out.println(o);
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行报错
java.lang.NoSuchMethodException: com.devilyuan.pattern.singleton.register.EnumSingleton.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
发现是无参构造方法报错,怀疑可能是只能调用有参构造方法。于是通过jad 下载了反编译软件,使用jad EnumSingleton.class
public final class EnumSingleton extends Enum
{
public static EnumSingleton[] values()
{
return (EnumSingleton[])$VALUES.clone();
}
public static EnumSingleton valueOf(String name)
{
return (EnumSingleton)Enum.valueOf(com/devilyuan/pattern/singleton/register/EnumSingleton, name);
}
private EnumSingleton(String s, int i)
{
super(s, i);
}
public Object getData()
{
return data;
}
public void setData(Object data)
{
this.data = data;
}
public static EnumSingleton getInstance()
{
return INSTANCE;
}
public static final EnumSingleton INSTANCE;
private Object data;
private static final EnumSingleton $VALUES[];
static
{
INSTANCE = new EnumSingleton("INSTANCE", 0);
$VALUES = (new EnumSingleton[] {
INSTANCE
});
}
}
经反编译后的代码,原来enum类都要继承Enum,并且只有有参构造方法,这里我将原来的无参构造方法改为有参构造方法。
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);
Object o = c.newInstance();
System.out.println(o);
} catch (Exception e) {
e.printStackTrace();
}
}
}
依然报错:
java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
这个错误和在懒汉式(静态内部类)构造方法中判断是否已经初始化,来解决反射破坏有异曲同工之妙,只不过后者是用户自定义,前者是官方定义
根据错误信息探寻源码:
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
标识:修饰符是枚举不可以反射。 探寻源码2:
//从容器中获取
public static <T extends Enum<T>> T valueOf(Class<T> enumType,
String name) {
T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumType.getCanonicalName() + "." + name);
}
//容器
Map<String, T> enumConstantDirectory() {
if (enumConstantDirectory == null) {
T[] universe = getEnumConstantsShared();
if (universe == null)
throw new IllegalArgumentException(
getName() + " is not an enum type");
Map<String, T> m = new HashMap<>(2 * universe.length);
for (T constant : universe)
m.put(((Enum<?>)constant).name(), constant);
enumConstantDirectory = m;
}
return enumConstantDirectory;
}
private volatile transient Map<String, T> enumConstantDirectory = null;
结论:枚举登记从JAVA层面已经解决了反射破坏的问题,并且从结构层面解决了多线程的问题,因为枚举在类声明的时候已经创建好了放到了一个Map容器中
2、实现序列化接口Serializable通过反序列化破坏
2.1 比如饿汉式单例(实现了序列化接口Serializable)进行反序列化破坏
序列化 就是把内存中对象的状态转换为字节码的形式,把字节码通过IO输出流,写到磁盘上永久的保存下来,称之为持久化。
反序列化 将赤计划的字节码内容,通过IO输入流读到内存中来,转化为一个JAVA对象
public class SerializableSingleton implements Serializable {
private final static SerializableSingleton SERIALIZABLE_SINGLETON = new SerializableSingleton();
private SerializableSingleton(){}
public static SerializableSingleton getInstance(){
return SERIALIZABLE_SINGLETON;
}
}
public class SerializableSingletonTest {
public static void main(String[] args) {
SerializableSingleton serializableSingleton = null;
SerializableSingleton serializableSingleton2 = SerializableSingleton.getInstance();
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream("SerializableSingleton.obj");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(serializableSingleton2);
objectOutputStream.flush();
objectOutputStream.close();
FileInputStream fileInputStream = new FileInputStream("SerializableSingleton.obj");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
serializableSingleton = (SerializableSingleton) objectInputStream.readObject();
objectInputStream.close();
System.out.println(serializableSingleton);
System.out.println(serializableSingleton2);
System.out.println(serializableSingleton == serializableSingleton2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
分析: serializableSingleton赋值null, serializableSingleton2赋值初始化,然后serializableSingleton2写入到磁盘中SerializableSingleton.obj文件(此过程为序列化),然后再从该文件读取出来,赋值给serializableSingleton(反序列化),最终比较两者。 若相等则是单例,反之则不为。
输出结果为false,很显然破坏了单例。
将代码调整,解决序列化反序列化破坏单例的问题
public class SerializableSingleton implements Serializable {
private final static SerializableSingleton SERIALIZABLE_SINGLETON = new SerializableSingleton();
private SerializableSingleton(){}
public static SerializableSingleton getInstance(){
return SERIALIZABLE_SINGLETON;
}
//反序列化
public Object readResolve(){
return SERIALIZABLE_SINGLETON;
}
}
分析:
较之调整前的差异,只不过在该类添加了readResolve()
方法,注意此处必须要写成这种方式。这里其实也是用到了桥接模式。
探寻源码:
在反序列化代码objectInputStream.readObject()
中调用Object obj = readObject0(type, false);
进入readObject0(type, false);
方法。
case TC_ENUM:
if (type == String.class) {
throw new ClassCastException("Cannot cast an enum to java.lang.String");
}
return checkResolve(readEnum(unshared));
case TC_OBJECT:
if (type == String.class) {
throw new ClassCastException("Cannot cast an object to java.lang.String");
}
return checkResolve(readOrdinaryObject(unshared));
注意这里TC_ENUM
和TC_OBJECT
,前者是对枚举,后者是对该饿汉式案例。
根据TC_ENUM
进入readEnum(unshared)
Enum<?> en = Enum.valueOf((Class)cl, name);
result = en;
这里发现,枚举其实是通过类名和类对象类找到一个唯一的的枚举对象,所以也就不存在多次的可能性,故而枚举不能被序列化和反序列化。
根据TC_OBJECT
进入readOrdinaryObject(unshared)
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) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, obj = rep);
}
}
return obj;
这里就是先判断hasReadResolveMethod
然后调用invokeReadResolve
方法就是反射调用readResolve
,最后将结果对象返回。这就意味着不会再重新创建对象。
3、多线程
比如上述提到的懒汉式(DCL)以及加锁的案例。都必须添加synchronized
保证线程安全性。
4、指令重排序
比如上述提到的懒汉式(DCL)案例中lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
这段代码,其实在JVM层面已经转换为多条指令。
- 指令1 分配对象的内存空间
- 指令2 初始化对象
- 指令3
设置
lazyDoubleCheckSingleton
指向刚分配的内存地址
经过重排序后如下:
- 指令1 分配对象的内存空间
- 指令2
设置
lazyDoubleCheckSingleton
指向刚分配的内存地址,此时对象还没被初始化 - 指令3 初始化对象
故而添加volatile
来阻止指令重排序带来的单例破坏影响。
5、实现Cloneable接口通过深克隆破坏
因为深克隆相当于重新new了。要解决只需要禁止深克隆即可。
5.1 单例类不实现Cloneable接口
public class Prototype implements Serializable{
}
5.2 重写clone()方法,在clone方法中返回单例对象即可。
protected Object clone() throws CloneNotSupportedException {
return INSTANCE;
}
6、通过不私有化构造方法破坏单例
单例类的构造方法没有私有化,那么就可以通过new创建。
- 感谢你赐予我前进的力量