单例模式实现最好的方式即枚举实现

单例类作为23种设计模式当中最常用的设计模式,实现方式有很多种,比较流行的是DCL(DoubleCheckLock)双重检查的实现,线程安全,又比较好,除了存在序列化的问题之外,还算不错,如果对DCL模式还不熟悉的可以看下我之前的博客,: 如何破坏双重校验锁的单例模式

最完美的实现方式其实是枚举,你用其他方式去实现单例,需要考虑很多问题,线程安全,序列化对单例模式的破坏。
关于What is an efficient way to implement a singleton pattern in Java?,stackOverflow有一条高赞的回答,如下图所示

在这里插入图片描述
EffectiveJava中明确表达过一个观点:
使用枚举实现单例的方法虽然还没有被广泛采用,但是单元素的枚举类型已经成为实现Signleton的最佳方法

其实在单例模式中,最不容易控制的问题是线程安全问题。
如果我们用代码实现单例,仅仅需要几行代码就可以解决

public enum Singleton {
	INSTANCE;
	public void 
}

接下来我们再看双重锁校验的代码

public class Singleton implements Serializable {
    private static volatile Singleton singleton;

    private Singleton() {
    }

    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

	// 防止序列化
    private Object readResolve() {
        return singleton;
    }

}

通过对比我们发现代码比较臃肿,这是因为大部分代码都是在线程安全和锁粒度之间做权衡,另外还要解决反序列化破坏单例模式的问题,不知不觉代码就写得复杂了,反观枚举类型,简洁明了

其实并不是使用枚举就不需要保证线程安全,只不过线程安全的保证不再需要我们关心而已,也就是说,其实在底层还是做了线程安全方面的保证的。

定义枚举时使用的enum和class一样,也是Java中的一个关键字,就像class对应一个Class类一样,enum也对应一个Enum类
我们用javac 编译下文件,然后再用jad工具执行jad SingletonEnum.class会生成Singleton.jad文件,我们可以直接用文本编辑器查看

public final class SingletonEnum extends Enum
{

    public static SingletonEnum[] values()
    {
        return (SingletonEnum[])$VALUES.clone();
    }

    public static SingletonEnum valueOf(String s)
    {
        return (SingletonEnum)Enum.valueOf(other/SingletonEnum, s);
    }

    private SingletonEnum(String s, int i)
    {
        super(s, i);
    }

    public void method()
    {
    }

    public static final SingletonEnum INSTANCE;
    private static final SingletonEnum $VALUES[];

    static 
    {
        INSTANCE = new SingletonEnum("INSTANCE", 0);
        $VALUES = (new SingletonEnum[] {
            INSTANCE
        });
    }
}

可以看到代码中有一个static修饰的静态代码块,意味着在类加载阶段的加载阶段之后,会被调用进行初始化,那么我们知道,当一个Java类第一次被真正使用时静态资源被初始化,Java类的加载和初始化过程都是线程安全的,因为Java虚拟机在加载枚举类时,会使用ClassLoader的loadClass方法,而这个方法使用同步代码块保证了线程安全,如图所示
在这里插入图片描述
所以,创建一个enum类时线程安全的。也就是说我们定义的一个枚举在第一次被使用时,会被虚拟机加载并初始化,而这个过程是线程安全的。基于类加载的特性,这种实现方式天生就是安全的。

接着有人可能会说,枚举可以解决反序列化的问题吗?
答案是可以的
因为普通的Java类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象,所以,即使单例中的构造函数是私有的,也会被反射破坏,由于反序列化后的对象是重新new出来的,所以这就破坏了单例模式。
但是枚举的反序列化并不是通过反射实现的,也就不会发生反序列化导致的破坏问题
在对枚举进行序列化是Java仅将枚举对象name属性输出到结果中,反序列化则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象的,同时,编译器是不允许任何对这种序列化机制的定制的,因此仅用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法的,
valueOf方法如下:
在这里插入图片描述

上述代码会尝试从调用enumType这个class对象的enumConstantDirectory()方法返回的map中获取名字为name的枚举对象,如果不存在就抛出异常,我们接着来看这个方法调用了什么
在这里插入图片描述

核心代码在于getEnumConstantsShared(),这一步获取到了一个map对象并将其赋值给enumConstantsDirectory,而这个方法又以反射的方式调用了enumType这个类型的values()静态方法,也就是上面编译器帮我们创建的方法,
在这里插入图片描述

    public static SingletonEnum[] values()
    {
        return (SingletonEnum[])$VALUES.clone();
    }

根据Java规范的规定,每一个枚举类型及其定义的枚举变量在JVM中都是唯一的,也就是说,每一个枚举项在JVM中都是单例

PS:破坏单例的方法

public class Singleton {
	private static volatile Singleton singleton;
	private Singleton() {
	}

	public static Singleton getSingleton() {
		if (singleton == null) {
			synchronized(Singleton.class) {
				if (singleton == null) {
					singleton = new Singleton();
				}
			}
		}
		return singleton;
	}
	
}
public class SerializableDemo1 {
	public static void main(String[] args) throws Exception {
		ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
		oos.writeObject(Singleton.getSingleton());
		File file = new File("tempFile");
		ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
		Singleton newInstance = (Singleton)ois.readObject();
		// 结果为false
		System.out.println(newInstance == Singleton.getSingleton());
	}
}

通过对Singleton进行序列化与反序列化得到的对象是一个新的对象,这就破坏了Singleton的单例性。

通过上面的代码我们可以知道,关键代码在于readObject()这个方法,它里面的调用顺序是readObject0() -> readOrdinaryObject() -> checkResolve()

private Object readOrdinaryObject(boolean unshared) {
	// 省略部分代码
	try {
		// 关键代码
		// isInstantiable():如果一个serializable/externalizable的类可以在运行时被实例化,
		// 那么该方法就返回true
		// true表示通过反射的方式调用午餐构造方法新建一个对象
		// 序列化会通过反射调用无参构造方法创建一个新的对象
		obj = desc.isInstantiable() ? desc.newInstance() :null;
	} catch(Exception e) {
	// ....
	}

	// 省略部分代码
	// 关键代码
	// hasReadResolveMethod():如果实现了serializable/externalizable接口中的类包含了readResolve则返回true
	// invokeReadResolve():通过反射的方式调用被反序列化的类的readResolve()方法
	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 = resp);
		}
	}
	return obj;
}
public class Singleton implements Serializable {
	// 与上面一样
	// ....
	// 新添加的方法
	private Object readResolve() {
		return singleton;
	}
}

再运行上面的类就会发现两个对象比较结果为true

  • 20
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
1) 优秀的程序应该是这样的:阅读时,感觉很优雅;新增功能时,感觉很轻松;运行时,感觉很快速,这就需要设计模式支撑。2) 设计模式包含了大量的编程思想,讲授和真正掌握并不容易,网上的设计模式课程不少,大多讲解的比较晦涩,没有真实的应用场景和框架源码支撑,学习后,只知其形,不知其神。就会造成这样结果: 知道各种设计模式,但是不知道怎么使用到真实项目。本课程针对上述问题,有针对性的进行了升级 (1) 授课方式采用 图解+框架源码分析的方式,让课程生动有趣好理解 (2) 系统全面的讲解了设计模式,包括 设计模式七大原则、UML类图-类的六大关系、23种设计模式及其分类,比如 单例模式的8种实现方式、工厂模式的3种实现方式、适配器模式的3种实现、代理模式的3种方式、深拷贝等3) 如果你想写出规范、漂亮的程序,就花时间来学习下设计模式吧课程内容和目标本课程是使用Java来讲解设计模式,考虑到设计模式比较抽象,授课采用 图解+框架源码分析的方式1) 内容包括: 设计模式七大原则(单一职责、接口隔离、依赖倒转、里氏替换、开闭原则、迪米特法则、合成复用)、UML类图(类的依赖、泛化和实现、类的关联、聚合和组合) 23种设计模式包括:创建型模式:单例模式(8种实现)、抽象工厂模式、原型模式、建造者模式、工厂模式。结构型模式:适配器模式(3种实现)、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式(3种实现)。行为型模式:模版方法模式、命令模式、访问者模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式(Interpreter模式)、状态模式、策略模式、职责链模式(责任链模式)2) 学习目标:通过学习,学员能掌握主流设计模式,规范编程风格,提高优化程序结构和效率的能力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

coffee_babe

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值