前言
单例模式是平时接触可能最多的一种设计模式,同时相对来说也算是比较简单的设计模式。但是依然有一些细节的地方需要注意。
首先什么是单例?按字面意思那就是只允许有一个实例对象。因为对于java类而言,我们可以通过new的方式创建很多的java对象,对象是需要占用我们的内存空间,而我们在实际的业务当中并不需要这么多对象,只需要一个对象就可以满足我们的业务需求了。比如web开发中,我们的controller类,正常情况下只需要存在一个就可以了。还有一些工具类,只需要一个实例就可以处理上传文件,加载配置文件等功能。相对比较安全的写法大概有如下5种。
一、饿汉模式(推荐)
public class Demo1 {
//直接在这里就初始化,管它需不需要,这就饿汉模式,饥渴难耐,必须先初始化
private static Demo1 demo1=new Demo1();
//私有化构造方法,必须
private Demo1(){}
public static Demo1 getInstance(){
return demo1;
}
}
首先静态的成员对象会随着类的加载而初始化,而且jvm会保证只会有一个,所以这里是完全能保证只有一个对象。虽然这种方式感觉很low,还有很多人说走来就创建加载对象是不是有点不太好,占内存,没有做到按需加载。但按我的理解感觉咱也不能这么吝啬吧,这点内存都不提前给。一个大型的项目里面,实际我们自己业务封装的单例对象也算不上特别多。
二、懒汉模式
public class Demo2 {
//静态成员变量初期不初始化对象,懒得先初始化
private static Demo2 demo2;
//私有化构造方法,必须
private Demo2(){}
public synchronized static Demo2 getInstance(){
if(demo2==null){//1.判断对象为空则初始化,否则直接返回
demo2=new Demo2();//2
}
return demo2;
}
}
加上synchronized 关键字的目的是给方法上锁。如果不这样做,那么在多线程并发的情况下,假如第一个线程A进入到1的位置,判断demo2为空然后进入到2的位置,但还没来得及执行代码,切换到了线程B进来,然后在1的位置发现还是为空,那么也进入到2的位置。这个时候线程A和线程B就会出现创建两个对象的情况。但是这样把方法锁住了,虽然说能够完全保证对象只有一个,但实际上也造成了很多其它线程在外面等待,性能上的相对就不是很好,所以一般不按照这种写。
三、双重锁-double check
public class Demo3 {
//静态成员变量初期不初始化对象
private static volatile Demo3 demo3;
//私有化构造方法,必须
private Demo3(){}
public static Demo3 getInstance() {
if (demo3 == null) {//1.判断对象为空则初始化,否则直接返回
synchronized (Demo3.class) {//2.给对象上锁
if(demo3==null){//3.再次判断是否为空
demo3 = new Demo3();//4.初始化
}
}
}
return demo3;
}
}
这种写法在网上的呼声还是比较高,首先是按需加载,然后又相对保证了性能。因为把同步方法改成了同步代码块,同时前面有一个判断,那么类实例存在直接返回,也就不需要等待。但是写法相对没那么简单。
问题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.实例指向对应的地址
四、内部类(推荐)
public class Demo4 {
private static class Demo4Holder{//定义一个内部类
private static Demo4 demo4=new Demo4();//初始化方法
}
//私有化构造器,必须
private Demo4(){}
public Demo4 getInstance(){
return Demo4Holder.demo4;//获取实例
}
}
这种方式是在类被引用的时候才进行初始化操作,同时jvm能够确保只会有一个实例,写法也比较简单。所以也是一种比较好的写法。
五、 枚举(实际业务用得不多)
public enum Demo5 {
INSTANCE; //直接申明一个变量
public void hello(){
System.out.println("hello world");
}
}
public class Test {
public static void main(String[] args) {
//直接访问则返回一个实例
Demo5 demo5 = Demo5.INSTANCE;
demo5.hello();
}
}
输出:
hello world
枚举这种方式是effective java作者推荐的一种写法,简单明了!而且最重要的是防反射和反序列化,上面的方法默认都不能做到防反射和防反序列化,必须做一些特殊处理才行。但我们开发肯定是对类相对会熟悉一点,这种枚举方式虽然简单明了,但在实际业务当中用得并不多。