Java中的单例模式的优秀实现

来源:互联网 发布:二重积分的算法 编辑:程序博客网 时间:2024/05/01 21:17

单例模式不得不说应该算初学者接触的最早几个设计模式之一了,主要是因为它的应用场景比起什么其他模式太简单易懂了,我们都知道,你要一个总体控制的类,比如一个能够初始化功能,提供特定功能的Helper类,那你用单例模式实现是非常有用的,因为它能提供两个非常好的优势

1、内存空间占用的优化,无论在哪里在何时都是同一个对象同一个实例,节省了内存

2、全局的同步化,由于是用同一个对象,对象的某些状态得到了同步,也就是说避免了不一致状态

但是,这个简单的设计模式看着很容易,实现起来却非常复杂(虽然初学者觉得非常简单),想要实现一个“可靠”的单例模式可要费一番脑筋,让我们来看看实际实现代码

实现方式1:

public class SingletonA {             /**       * 单例对象实例       */        private static SingletonA instance = null;             public static SingletonA getInstance() {            if (instance == null) {                                            instance = new SingletonA();                     }            return instance;        }    }    
这就是最简单的实现方式,也是我们最先接触到的,对于大一的学生来说,你能知道这个就已经很厉害了哈哈~

言归正传,这个实现方式实际上就是“懒汉模式”,不要小看它,它可用到了Lazy Load的思想,就是延迟加载,只有当你第一次需要这个单例类的时候,你才会调用

instance = new SingletonA(); 

这句代码,那么没用到之前都不用占用内存空间,这可是一种优化方式呢!

但是这个简单的实现一旦到了多线程情况下就会遇到很大的问题,假设线程A、B在执行中,A先做了判断,发现instance为空,正当它准备执行下一句的时候,JVM将处理器资源分配给了线程B,它又做了一次判断,但B也发现instance为空,因为A还没有执行下一句,内存还未分配,于是B执行了初始化new操作,当JVM把A唤醒的时候,它又进行了一次初始化,这样你的instance就做了两次初始化,就出现了多个instance实例,就不满足第一点了,所以这个方法不是线程安全的,那么如何能够实现一个线程安全的单例模式呢?


实现方式2:

public class SingletonB {             /**       * 单例对象实例       */        private static SingletonB instance = null;             public synchronized static SingletonB getInstance() {            if (instance == null) {                instance = new SingletonB();            }            return instance;        }    }    
说到同步我们怎么会忘了synchronized关键字呢?既然这个代码段要保证线程安全,用关键字就是啦!虽然没错,但你这样的代码有个很大的性能问题,当你第一次创建实例之后,后面都不需要同步了对不对(反正都是同一个了),可进入同步锁和释放锁的耗时可是很大的,仅仅为了一开始的一次操作放弃了后面全部的性能,这可真的是得不偿失,所以说这个方式虽然满足了多线程要求,性能上的要求完全不满足!


实现方式3:

public class SingletonC {             /**       * 单例对象实例       */        private static SingletonC instance = null;             public static SingletonC getInstance() {            if (instance == null) {                synchronized (SingletonC.class) {                    if (instance == null) {                        instance = new SingletonC();                    }                }            }            return instance;        }    }    

既然上面的性能上不行,那我们可以来尝试做优化呀~把需要线程安全的状态用同一个同步锁来保护,这可是《并发编程实践》提供的思路对不对,那我这样优化岂不是很完美,假设线程A进入了同步代码块中,另外一个线程B即时也想进入代码块(判断为null,表示true)但B就被阻塞了,当A完成初始化之后B即时再进入也不会有问题,因为还有一次判断。而完成第一次初始化之后以后一旦判断为false,自然也不会进入同步代码块。可以说这种方式兼容了性能和并行的要求,感觉好像已经很完美了,实际上这个方法也有个名字就是DCL(double check lock),并且在一段时间一直被认为是一种很好的单例并发写法,但这个方法在java环境下却有着隐藏非常深的BUG

我不说那么详细,只是粗略的用一个例子做比方说一下BUG的所在点:

当你执行instance = new SingletonC();这个操作的时候,实际上执行了三个步骤,分配内存、初始化以及把instance指向内存,按道理这是很正常的三个步骤,但是这三个步骤JVM执行的时候是可以乱序执行的,也就是说它认为这三个步骤反正都要做,而且很快就做完了,你颠倒顺序也不会出现问题。为什么这么说呢?举个例子,你炒菜如果不是太计较的话,比如我们的食堂,你先放盐再放醋感觉也没什么问题对不对…

可是,假设JVM先执行了第一、第三步骤,还没有执行第二步骤,这时候你的A线程被强制切换了,第二个线程B进入了代码的第一个判断(还没有进入同步代码段),它的判断发现instance竟然不是null,因为A中第三个步骤已经执行了,所以线程B连同步代码块都不会进入,然后它就进入了return的执行。这时候B得到的instance实例还没有初始化,就是说它指向的内存区域我们完全不可知是什么,但却误认为它就是初始化完成的实例,那程序就进入了不可控的阶段,自然而然BUG就来了!用我们之前的例子,假设同学们都认为只要放了盐菜就可以吃了,可烧菜的同学(线程A)却先放了盐,这时另外一个同学来了(线程B),他一看放了盐,直接就端走去吃了,可这时候的菜却并没有完成制作,问题就出现了。

官方在发现问题后新加入了volatile关键字来优化这个问题,JDK1.5以及之后的版本只要在instance的定义中加入volatile关键字,作用是保证每次都从主内存中读取对象,那么就可以解决DCL失效的情况,但同样,性能上肯定会有所消耗。


实现方式4:

public class SingletonD {             /**       * 单例对象实例       */        private static SingletonD instance = new SingletonD();             public static SingletonD getInstance() {            return instance;        }    }    

我第一次看的时候也看蒙了,这就是“饿汉”式的写法,它可以保证并发性,为什么呢?我们要知道,一个类在被加载的时候,JVM能够保证它只被加载一次,而我们也知道,static变量会在类加载时就被创建,而不是在实例化的时候,在它那这样一来不就能够保证了单例对象的唯一性了吗?话是这样说没错,但这个方法也有一个缺陷,我们之前提到的延时加载(只有当第一次用到时才分配内存,达到分配内存的效果),这个模式就没有这个优点了,而这个缺陷的另外一个问题,假设你的单例模式初始化时需要一些实时参数,那么这个方式由于只能在类加载时初始化实例,所以自然也做不到这个要求,不过它为我们更好的单例模式提供了一个思路!


实现方式5:

public class SingletonE {             private static class SingletonHolder {            /**           * 单例对象实例           */            static final SingletonE instance = new SingletonE();        }             public static SingletonE getInstance() {            return SingletonHolder.instance;        }    }    


这就是《Java并发编程实战》推荐的单例模式的方式,也是我认为最好的一个方式,第一个并发性的问题由于instance是静态内部类,不会出现问题;并且SingletonHolder只能在内部被访问,只有第一次调用getInstance才会初始化,所以又保持了延时加载的特性,自然就可以做到初始化时参数的使用。

说了这么多实现方式,其实还有一些实现方式比较冷门,大家可以去看一看,但对于高并发的情况下,只会最基本的实现是不够的,从上面这五种的不断进化,最终静态内部类的方式应该来说已经满足了所有要求,可以说是优秀实现了。当然,肯定还会又很多更有效的实现方式等着大家去了解,我只希望能帮助大家理解这些实现的优劣。

1 0
原创粉丝点击