「大冰撸设计模式」java 创建型模式之单例模式

来源:互联网 发布:金灿荣舌战公知哪一期 编辑:程序博客网 时间:2024/06/05 04:01

今天起,笔者开始撸java和javascript的设计模式,先从java开始写起,再从java的设计模式中提取思想和方法,应用到javascript中,有兴趣的码友可以关注笔者,一起交流,共同进步。


单例模式

单例模式是常见的一种设计模式,在java应用中,单例模式可以保证某个对象在jvm中只有一个实例存在。这样的模式有几个好处:

  1. 某些类创建的过程比较繁琐,使用单例模式可以节省一大笔系统开销

  2. 省去了new操作符,降低了系统内存的使用频率,减轻GC压力

  3. 作为通信媒介使用,也就是数据共享,它可以在不建立直接关联的条件下,让多个不相关的两个线程或者进程之间实现通信。

只要有点java面向对象基础的都知道,将对象的静态实例和构造方法私有化,只提供一个公共的getInstasnce()方法来获取静态实例即可。看起来挺容易,但是实现起来还是有很多细节和难点需要考虑的。请看下图,首先我们先写一个简单的单例类:

「大冰撸设计模式」java 创建型模式之单例模式

单例模式

这个类可以满足基本的要求,但是,像这种没有线程安全保护的类,一旦把它放入多线程的环境,肯定就会出问题了,该怎么解决呢?我们首先想到的肯定是对getInstance加synchronized关键字,如下:

「大冰撸设计模式」java 创建型模式之单例模式

线程安全的getInstance方法

这样做,可以做到线程安全了,但是,synchrozed锁住的是这个对象,在getInstance方法上加synchronized,在性能上会有所下降。因为每次调用getInstance方法,都要在对对象上锁。而事实上,我们只需要在第一次创建对象的时候对对象上锁,之后就不需要了。所以,这个地方需要改进,我们可以改成下面这个:

「大冰撸设计模式」java 创建型模式之单例模式

性能较好的线程安全的getInstance方法

好了,我们已经解决了我们能想到的问题了,既解决了线程安全问题,又解决了性能问题。然而,我们万万没想到,JVM还给我们埋了一个坑。在java指令中,创建对象和赋值是分两步进行的,也就是说上图第17行的instance = new Singleton()是分两步执行的。但是JVM并不保证哪一步先执行,也就是说这两个操作的先后顺序是不能保证的。有可能JVM会先为新的Singleton实例分配空间,赋值给instance成员变量,然后再去初始化这个Singleton实例。这样就可能出错了,我们以A、B两个线程为例:

  1. A、B线程同时进入了第一个if判断

  2. A首先进入synchronized块,由于instance为null,此时执行instance = new Singleton();

  3. 由于JVM的优化机制,JVM选择了先为Singleton实例分配了一个空白的空间并赋值给instance成员变量(注意此时JVM并未开始初始化这个实例),然后A线程离开了synchronized块。

  4. B进入synchronized块,此时instance不为null,因此B马上离开了synchronized块。

  5. 此时B打算使用Singleton实例,去发现它没有被初始化,于是错误就发生了。

由此可见,程序还是有可能发生错误的,其实程序在运行过程中是很复杂的,从这点我们就可以看出,尤其是在写多线程环境下的程序更有难度,有挑战性。我们对改程序做进一步优化:

「大冰撸设计模式」java 创建型模式之单例模式

使用内部静态类来维护单例的实现

如上图,使用内部类来维护单例的实现,JVM内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。这样当我们第一次调用getInstance()的时候,JVM帮我们保证instance只被创建一次,并且会保证把赋值给instance的内存初始化完毕,这样我们就不必担心上面的问题了。同时,该方法也只会在第一次调用的时候使用互斥机制,这样也解决了低性能的问题。这样,我们暂时得到了一个近乎完美的单例模式:

「大冰撸设计模式」java 创建型模式之单例模式

近乎完美的单例模式

其实说它完美,也只是相对的,不是绝对,如果在Singleton的构造方法中抛出异常,实例将永远得不到创建,也会出错。所以说,十分完美的东西是没有的,我们只能根据实际情况,选择最适合自己应用场景的实现方法。也有人这样实现:因为我们只需在创建实例的时候进行加同步synchronized,所以只要将创建和getInstance()分开,单独为创建加synchronized关键字,也是可以的,如下:

「大冰撸设计模式」java 创建型模式之单例模式

单独为创建方法加synchronized关键字

考虑性能的话,整个程序只需创建一次实例,所以性能也不会有什么影响。

通过单例模式的学习告诉我们:

  1. 单例模式理解起来简单,但是具体实现起来还是有一定的难度。

  2. synchronized关键字锁定的是对象,在用的时候,一定要在恰当的地方使用(注意需要使用锁的对象和过程,可能有的时候并不是整个对象及整个过程都需要锁)。

到这儿,单例模式基本已经讲完了,结尾处,笔者突然想到另一个问题,就是采用类的静态方法,实现单例模式的效果,也是可行的,此处二者有什么不同?

首先,静态类不能实现接口。(从类的角度说是可以的,但是那样就破坏了静态了。因为接口中不允许有static修饰的方法,所以即使实现了也是非静态的)

其次,单例可以被延迟初始化,静态类一般在第一次加载是初始化。之所以延迟加载,是因为有些类比较庞大,所以延迟加载有助于提升性能。

再次,单例类可以被继承,他的方法可以被覆写。但是静态类内部方法都是static,无法被覆写。

最后一点,单例类比较灵活,毕竟从实现上只是一个普通的Java类,只要满足单例的基本需求,你可以在里面随心所欲的实现一些其它功能,但是静态类不行。从上面这些概括中,基本可以看出二者的区别,但是,从另一方面讲,我们上面最后实现的那个单例模式,内部就是用一个静态类来实现的,所以,二者有很大的关联,只是我们考虑问题的层面不同罢了。两种思想的结合,才能造就出完美的解决方案,就像HashMap采用数组+链表来实现一样,其实生活中很多事情都是这样,单用不同的方法来处理问题,总是有优点也有缺点,最完美的方法是,结合各个方法的优点,才能最好的解决问题!

此文是笔者在网上研究前辈写的文章,理解后写出,部分段落直接挪用了,文章出处:http://blog.csdn.net/zhangerqing/article/details/8194653

若有错误或者疑问,请在评论区指出错误或提出疑问,笔者会一一解答,一起交流,共同进步^_^
0 0