单例模式是平时接触可能最多的一种设计模式,同时相对来说也算是比较简单的设计模式。但是依然有一些细节的地方需要注意。 首先什么是单例?按字面意思那就是只允许有一个实例对象。因为对于java类而言,我们可以通过new的方式创建很多的java对象,对象是需要占用我们的内存空间,而我们在实际的业务当中并不需要这么多对象,只需要一个对象就可以满足我们的业务需求了。比如web开发中,我们的controller类,正常情况下只需要存在一个就可以了。还有一些工具类,只需要一个实例就可以处理上传文件,加载配置文件等功能。相对比较安全的写法大概有如下5种。
首先静态的成员对象会随着类的加载而初始化,而且jvm会保证只会有一个,所以这里是完全能保证只有一个对象。虽然这种方式感觉很low,还有很多人说走来就创建加载对象是不是有点不太好,占内存,没有做到按需加载。但按我的理解感觉咱也不能这么吝啬吧,这点内存都不提前给。一个大型的项目里面,实际我们自己业务封装的单例对象也算不上特别多。
加上synchronized 关键字的目的是给方法上锁。如果不这样做,那么在多线程并发的情况下,假如第一个线程A进入到1的位置,判断demo2为空然后进入到2的位置,但还没来得及执行代码,切换到了线程B进来,然后在1的位置发现还是为空,那么也进入到2的位置。这个时候线程A和线程B就会出现创建两个对象的情况。但是这样把方法锁住了,虽然说能够完全保证对象只有一个,但实际上也造成了很多其它线程在外面等待,性能上的相对就不是很好,所以一般不按照这种写。
这种写法在网上的呼声还是比较高,首先是按需加载,然后又相对保证了性能。因为把同步方法改成了同步代码块,同时前面有一个判断,那么类实例存在直接返回,也就不需要等待。但是写法相对没那么简单。 问题1:为什么步骤3的位置还需要进行一个空判断 同样的因为在高并发的情况下,如果不进行空判断,A线程到了步骤4的位置,但是还没有执行代码,那么线程切换到B,B进入到步骤1,发现实例为空,那么继续往下在步骤2等待。这时候切换到A线程完成实例化。那切换到B线程,如果不判断,也会进行第二次new对象,所以这里必须要判断。 问题2:为什么要加volatile关键字 这里的话就涉及到cpu和jvm执行指令的问题了,他们会对指令进行优化,就是可能进行重新排序。而demo2=new Demo2() 这个在生成指令的时候是如下3个步骤,关键在于2,3步骤可能会出现调换的情况,也就是说我对象还没有初始化完成,就已经指向了一个地址,那么这个时候另外的线程实际上得到的对象是有问题的。具体可以参考volatile关键字说明
memory = allocate(); // 1:分配对象的内存空间 ctorInstance(memory); // 2:初始化对象 instance = memory;//3.实例指向对应的地址这种方式是在类被引用的时候才进行初始化操作,同时jvm能够确保只会有一个实例,写法也比较简单。所以也是一种比较好的写法。
枚举这种方式是effective java作者推荐的一种写法,简单明了!而且最重要的是防反射和反序列化,上面的方法默认都不能做到防反射和防反序列化,必须做一些特殊处理才行。但我们开发肯定是对类相对会熟悉一点,这种枚举方式虽然简单明了,但在实际业务当中用得并不多。