Java设计模式之单例模式 (2)

可以看到,以上的第二种方法只要调用getInstance方法,就会走到同步代码块里。因此,会对效率产生影响。其实,我们完全可以先判断实例是否已经存在。若已经存在,则说明已经创建好实例了,也就不需要走同步代码块了;若不存在即为空,才进入同步代码块,这样可以提高执行效率。因此,就有以下双重检测了:

public class Singleton { //注意,此变量需要用volatile修饰以防止指令重排序 private static volatile Singleton singleton = null; private Singleton(){ } public static Singleton getInstance(){ //进入方法内,先判断实例是否为空,以确定是否需要进入同步代码块 if(singleton == null){ synchronized (Singleton.class){ //进入同步代码块时也需要判断实例是否为空 if(singleton == null){ singleton = new Singleton(); } } } return singleton; } }

需要注意的一点是,此方式中,静态实例变量需要用volatile修饰。因为,new Singleton() 是一个非原子性操作,其流程为:

a.给 singleton 实例分配内存空间 b.调用Singleton类的构造函数创建实例 c.将 singleton 实例指向分配的内存空间,这时认为singleton实例不为空

正常顺序为 a->b->c,但是,jvm为了优化编译程序,有时候会进行指令重排序。就会出现执行顺序为 a->c->b。这在多线程中就会表现为,线程1执行了new对象操作,然后发生了指令重排序,会导致singleton实例已经指向了分配的内存空间(c),但是实际上,实例还没创建完成呢(b)。

这个时候,线程2就会认为实例不为空,判断 if(singleton == null)为false,于是不走同步代码块,直接返回singleton实例(此时拿到的是未实例化的对象),因此,就会导致线程2的对象不可用而使用时报错。

4)使用静态内部类

思考一下,由于类加载是按需加载,并且只加载一次,所以能保证线程安全,这也是为什么说饿汉式单例是天生线程安全的。同样的道理,我们是不是也可以通过定义一个静态内部类来保证类属性只被加载一次呢。

public class Singleton { private Singleton(){ } //静态内部类 private static class Holder { private static Singleton singleton = new Singleton(); } public static Singleton getInstance(){ //调用内部类的属性,获取单例对象 return Holder.singleton; } }

而且,JVM在加载外部类的时候,不会加载静态内部类,只有在内部类的方法或属性(此处即指singleton实例)被调用时才会加载,因此不会造成空间的浪费。

5)使用枚举类

因为枚举类是线程安全的,并且只会加载一次,所以利用这个特性,可以通过枚举类来实现单例。

public class Singleton { private Singleton(){ } //定义一个枚举类 private enum SingletonEnum { //创建一个枚举实例 INSTANCE; private Singleton singleton; //在枚举类的构造方法内实例化单例类 SingletonEnum(){ singleton = new Singleton(); } private Singleton getInstance(){ return singleton; } } public static Singleton getInstance(){ //获取singleton实例 return SingletonEnum.INSTANCE.getInstance(); } }

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wssxzp.html