关于单例

-> 什么是单例

单例是指这样一种类的设计,它确保该类在程序运行期间有且只有一个实例(对象),即其他所有的代码共享这个唯一的实例。

单例一般由该类提供的一个公共的静态工厂方法取得。

习惯上这个方法一般起名为: getInstance

-> 为何需要单例

有一些功能和状态,你希望能全局共享。

这种全局共享可以通过定义一个只有静态方法和静态成员的类来实现,但有时你可能更希望操作的是一个对象而不是一个类,因为对象可能更灵活,状态更易保存等等。

-> 如何实现单例设计

1 定义构造方法为private
2 定义类为final,使其不能被继承。注:因为private的构造方法已经实际上实现了"不能被继承",所以这一步可以省略。
3 定义一个公共的静态工厂方法,用来取得一个唯一的实例。

一个基本的单例实现




这个实现在大部分情况下都是合适的,但是有极少数的情况下:你希望这个单例不到最后一刻(马上要用的时候),不要实例化。

-> 最后一刻实例化(Lazy Instantiation)




在一个单线程的环境里,这个代码满足了"最后一刻实例化"的需求,但是它不是线程安全的,于是
-> 线程安全的"最后一刻实例化"




好,这个代码是线程安全的了。可是问题又来了:你每次调用 getInstance() 方法,都会有一个"取得同步锁,释放同步锁"的过程,这个过程在实例化完成以后就成了无意义的成本,甚至有的时候这个成本都超过了实例化本身,换句话说你还不如老老实实用前面那个最基本的实现呢。

程序猿就是爱思考,既然只有第一次有意义,那咱就想办法只同步第一次

-> 最后一刻实例化:双重检查的同步




这个想法是好的,可惜在java中,是行不通的(编译器会给出警告)。为什么呢?这里牵扯到多线程的原理和java处理reference的方式,本文不作深入,简单的说,当同步锁的主人改变的时候,在一个极为微小的时间段内,两个线程所看到的 "INSTANCE" 这个 reference 是不一样的。为了解决这个问题,java引入了volatile这个关键字。

-> 使用了volatile的双重检查




这个没问题了。

呃,其实还有一个非常非常微不足道的问题:它不够优雅,而且每次都进行 if() 检查,这个检查在第一次以后就显得挺傻的。

好吧,这年头只有偏执狂才能生存,you have to be paranoid to survive.

-> 利用了加载机制的"最后一刻实例化"




如果你想一下,在中什么是"最后一刻完成",同时又是"天生线程安全"的呢?
答桉是:类的加载。

这就是用优雅的代码,尽可能让已有的机制帮你实现。
后续我准备说到 mutable 和 immutable 的单例的,有争议的说,如果是 mutable(stateful),有些时候可能并不适合用单例。

你说的这个显然是mutable。

硬要用的话,我会设计成这样:


另外,单例只是实例控制的一种情况(极限的情况)。

"实例控制"的真义在于,对类的实例化进行控制,而究竟有几个实例并不重要,看需求

-> Immutable 的单例

有一条封装的原则是:尽量限制能改变对象状态的操作,仅提供绝对必要的此类操作。

这条原则的一种极限情况是:杜绝所有能改变对象状态的操作。这就是immutable。



所谓immutable,就是指一个对象在建立以后,其状态就不会再发生改变(immutable的设计并不限于单例)。

没有状态(stateless),即只有方法没有成员变量,等于状态不能改变,即immutable。
源生类型的值与reference的值,天生immutable。

简单的说,immutable的设计是这样的:

1- immutable类被设计为final,不能被继承
2- 所有的成员变量被设计为private final,在对象被建立的时候赋值,然后不能再改变
3- 成员变量中,对于也是immutable的(包括 primitive type),不用作更多处理,对于mutable的(状态能被改变的),需要特别保护,所谓特别保护就是确保外界自始至终拿不到其reference

immutable的对象最大的好处就是天生线程安全,用起来不必考虑任何同步的问题。
设计为单例的很多是为了提供一组全局可见的功能(方法),这样的单例没有状态,是immutable的。

相对于直接提供静态方法,单例的设计更灵活,由于工厂方法隐藏了实例化的过程,用户不必知道返回的实例究竟是哪种实现,而只知道公共的方法签名,设计者可以在后续的开发中很方便的偷梁换柱,而完全不会影响到客户端代码。
而如果把default(package private)作为等同于private来设计的话(即假设所有的客户端代码都不会再使用同一个包名的话),你甚至可以在这样的设计里使用包内可见的继承(破坏了immutable类的"不能被继承原则",不是严格意义上的immutable)。

-> 单例的代码实例之一

咱们来设计一个类,它的功能只有一个,就是对一组数据排序,我们决定把它设计为单例。

一开始,我们想到了两个方法,来对一个数组排序。
到时候怎么实现先不用管,反正方法签名大概是这样的:


-> 单例的代码实例之二

考虑到使用的时候也许会有"只想排序一部分"的情况,我们再加两个方法

然后我们发现,通过一个万能的SafeComparator,最终所有的调用都集中在一个方法上面:


-> 单例的代码实例之三

这时我们确定,需要具体实现的就是第一个abstract的方法,做一个静态的内部类来实现它。

就先做一个简单的冒泡排序好了。

其中用到了一个swap方法,这个方法交换两个元素的位置,是任何的排序都可能用到的,我们把它设计为private的静态方法。


注意中是直接实现的公共方法,一定要作参数检查,而且这个参数检查不能用assert了事。


-> 单例的代码实例之四

罗嗦了这么多,正题来了,把 Sort 设计为单例:


-> 单例的代码实例之五

这时我们学会了更风sao的快速排序,实现它,并把反回的单例改成快速排序的实现。


这时所有的用户代码都不需要改动,程序的其他用到了 Sort.getInstance() 的地方甚至都不需要知道有这样一个改变。

就好像你偷偷给你的车换了个更强力的引擎,司机不需要知道,他还是一样的开,他只会发现速度快了不少。




-> 单例的代码实例之六

这时我们发现,重复的参数检查是很傻的,由其是第归内的重复检查。
参数检查应该集中在父类完成,子类只管实现:


图片改小一点,试试重发完成版



-> mutable的单例

的单例在使用的时候只需要注意一点:线程安全。
注意是"使用的时候",也就是说,这个同步的工作是谁来用谁来做,这跟实例化的时候不同。
有极为少数的情况,你确定用户对单例对象的所有使用情况(包括现在和未来)都是一个单独的(不连续的)方法调用,这时为了方便起见,你可能选择让这个类自己实现同步。

你几乎永远不能确定上述情况。

mutable的单例使用,最常见的例子有设置管理(ConfigurationManager),缓存等。
一时想不到什么好说的了
就这么结了吧

用一个教JavaEE的老教授的话作结尾:
懂得技术虽然重要,但更重要的是懂得什么时候应该选什么技术。


这贴写的时候,比较简略,可能没说清楚为什么这种直接在方法上加synchronized关键字的写法不好。

有的同学刚刚甚至告诉我这样写比较“优雅”

为什么说这样的写法不好:
第一,它增加了【每次】调用的成本,你每次调用Singleton.getInstance(),都会比没有同步的版本慢了那么一点点,你调用的次数越多,这个成本累积起来就越高。想想你本来为什么要用“最后一刻实例化”,一般是因为实例化(new Singleton())的成本太高并且这个实例不一定会用到,“最后一刻实例化”的潜台词是“如果程序中用不到这个实例那就不要实例化”。如果这个“成本太高”是因为“时间成本太高”,那假如你直接在方法定义上加synchronized关键字,随着Singleton.getInstance()调用次数的增多,其额外的同步时间成本累积起来甚至有可能超过new Singleton()的时间成本,那么你的“最后一刻实例化”就变得毫无意义甚至适得其反。

第二,它暴露了该方法的“内部线程安全”的实现机制,暴露了其实现同步所依赖的对象锁——Singleton.class。从【封装】的角度来说,这个方法对其【线程安全的实现细节】没有封装好。如果这是一个API中的公开方法,那么【方法定义不能改动】使它放弃了将来使用其他手段实现线程安全的可能性,不仅如此,暴露出的对象锁还成为该方法的一个弱点,为有意或无意的“不恰当的同步”干扰这个方法的运行提供了潜在的可能。比如,运行下面的代码可以很容易阻挡其他线程对该方法的正常访问:
Executors.newSingleThreadExecutor().execute(new Runnable() {

@Override
public void run() {

synchronized(Singleton.class) {

while( true ) {

System.out.println("My precious!");
}
}
}
});

主要参考资料:
<Effective Java>第二版
网络维基:<Double-checked locking>
最后,我在本帖里提到了两次,还需要再强调一次:

“最后一刻实例化”在绝大多数情况下是不需要的。

来自java吧 那十无忧 大神 http://tieba.baidu.com/p/1633595310?pn=1



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值