[补课]Singleton的性能问题

最近我听到了这样一种说法:Singleton模式在多线程环境下存在性能问题。并且,这就成了Singleton模式的一个罪状:因为Singleton在多线程底下有性能问题,因为J2EE是多线程的,所以J2EE底下不应该用Singleton模式。现在我来帮这些擅长过度省略的高手们补补课:Singleton模式,究竟在怎样的情况下才有性能陷阱?

第一个问题,Singleton模式在多线程环境为何遭遇困境?答案是,采用lazy initialization策略时,如果没有合理的同步(synchronize),各个线程得到的实例可能不是同一个。详情可以参考JavaWorld 2001年的文章:When Is A Singleton Not A Singleton。(某些同志连这篇文章都没听说过,居然也可以抱怨说“找不到这方面的英文资料”,恩,我们说话恐怕还是谦虚谨慎点好。)


public class MyClass {
private static MyClass _instance;
public static MyClass getInstance() {
if(_instance != null) {
_instance = new MyClass();
}
return _instance;
}

如果有两个线程同时调用MyClass.getInstance()方法,就有可能造成MyClass的构造子被调用两次。所以我们需要同步――准确说,恰当的同步。在C++里面有一种常见的Singleton实现策略叫Double Checked Locking idiom(http://www.javaworld.com/javaworld/jw-01-2001/jw-0112-singleton_p.html,listing 6),但这种实现策略在Java中不生效(这是由于JVM的本性造成的,详情请看这篇文章:http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html ),因此如果要在Java中实现lazy initialization策略的Singleton,你就必须采取保守的同步策略,也就是:



public static [b]synchronized[/b] MyClass getInstance() {
...
}

如果采取保守的同步策略(将整个getInstance()方法同步),多个线程需要获得Singleton实例时就必须在getInstance()方法上排队等待。这就是传说中的“Singleton模式的性能问题”。现在我要提问了:这种性能问题在什么情况下才会出现?

答案就摆在你面前:只有采用lazy initialization策略时,才会存在这样的性能问题。那么如果放弃lazy initialization策略、改用eager initialization策略(即:预先创建好Singleton实例),Singleton模式还会存在这样的性能问题吗?我们把上面的例子改成eager initialization策略看看:


public class MyClass {
private static MyClass _instance = [b]new MyClass()[/b];
public static MyClass getInstance() {
return _instance;
}

我请问,这样的一个Singleton难道还会有什么“性能问题”吗?它付出的代价是更长的初始化时间,获得的收益则是更快并且线程安全的实例获得,而这正是Spring容器对其管理的组件的默认策略。其实这个问题早已有了定论,请看http://www.javaworld.com/javaworld/jw-01-2001/jw-0112-singleton_p.html这篇文章的listing 1和listing 2,Singleton模式的两种正确的实现策略早在2001年就已经讨论清楚了。

作为结论,我提醒某些善于过度简化乃至以讹传讹的高手们:请不要简单地说一句“Singleton模式有性能问题”了事。完整的说法应该是,当采用Lazy Initialization策略时,如果需要经常地获取Singleton实例,则Singleton模式中用于获取实例的方法有可能成为性能瓶颈;如果条件允许采用Eager Initialization策略,则Singleton模式不会带来任何额外的性能开销――如果考虑管理对象池或是新建对象实例的性能开销,Singleton模式能够提升系统的性能。

现在有这样一个问题:有这么一个客户端,用户可以定阅世界上所有城市的温度信息,每5分钟客户端刷新一次,那么服务器端肯定要在内存中做个cache,我的想法是做个hashmap来存储,然后这个hashmap位于一个单例中,因为城市太多,而且有的城市定制的人很少,所以采用lazy initialization的方式,只有当第一次被访问的时候,才缓冲该城市信息,并且5分钟后该信息自动失效,看了gxixi的文章,这里只能用同步的方式,这样会显著影响效率,那么究竟应该怎么做那?
如果说几千个数据还可以一次全部初始化,那么如果数据多的多该真么办那?

>
>
> 现在有这样一个问题:有这么一个客户端,用户可以定阅世界
> 纤谐鞘械奈露刃畔?,每5分钟客户端刷新一次,那么服务器端
> 隙ㄒ谀诖嬷凶龈cache,我的想法是做个hashmap来存储,然?
> 这个hashmap位于一个单例中,因为城市太多,而且有的城市定?
> 的人很少,所以采用lazy
> initialization的方式,只有当第一次被访问的时候,才缓冲该
> 鞘行畔?,并且5分钟后该信息自动失效,看了gxixi的文章,这里
> 荒苡猛降姆绞?,这样会显著影响效率,那么究竟应该怎么做?
> ?
>
> 如果说几千个数据还可以一次全部初始化,那么如果数据多的
> 喔谜婷窗炷??

你想清楚自己究竟要做什么了吗?


private Map _tempCache;
public Long getTemperature(String city) {
if(!_tempCache.contains(city)) {
Long result = calcTemperature(city);
_tempCache.put(city, result);
}
return (Long) _tempCache.get(city);
}

请问你,这里有任何一个地方需要同步吗?如果压根不需要同步,又哪来的什么“性能问题”呢?

你把这个问题避开了,你的MAP是什么?hashtable本身就是同步的,用在这里本身就是同步性能陷阱。
用hashmap,完全没有同步问题,可是无法保证1读写同一个键的互斥性,2会出现两个线程并发给一个key赋值得现象,如果值不是简单的long而是复杂的数据库查询结果的话,还是有很大隐患的。
我觉得解决方案还在这篇文章上,
http://www-900.ibm.com/developerWorks/cn/java/j-jtp07233/

或许你有什么更好的方法?
于诸君共勉:)

>>用hashmap,完全没有同步问题,可是无法保证1读写同一个键的互斥性,2会出现两个线程并发给一个key赋值得现象,如果值不是简单的long而是复杂的数据库查询结果的话,还是有很大隐患的。

照这个需求,“温度信息”是每过5分钟才更新一次的,我为什么要去保证它一秒钟内两次写操作互斥?如果说同一瞬间有两个请求过来了,很好,由它们去吧,那又怎么样呢?这个需求根本就用不着保证什么同步性互斥性,为什么要强加给它呢?

照这个需求,“温度信息”是每过5分钟才更新一次的
-------------------------------------------------
客户端5分钟刷新一次,服务器端就有可能每时每刻都在接受访问,很有可能出现 并发阿


我为什么要去保证它一秒钟内两次写操作互斥?如果说同一瞬间有两个请求过来了,很好,由它们去吧,那又怎么样呢?
--------------------------------
如果是put 数据库查询读来的value object list,put 的时间比较长,对象比较复杂 ,就会发生写-写冲突阿 这可以由他去么? 同理 读-写冲突你怎么解决? 你的意思好像是不管?即使是long这样的简单数据,也有可能有问题
所以觉得还要考虑同步互斥

>>客户端5分钟刷新一次,服务器端就有可能每时每刻都在接受访问,很有可能出现 并发阿

有并发又怎么样呢?并发冲突了又怎么样呢?有什么严重的后果?不妨看看代码:


if(!_cache.contains(city)) { // 现在还没这个城市的温度
// 突然这时候另一个线程访问了,cache里有这个城市的温度了
_cache.put(calcTemperature(city));
// 于是,cache里刚放进去的数据被冲掉
}

那又怎么样呢?无非是用下一秒钟的温度冲掉了上一秒的而已。除非真是《后天》的那种情况,否则我可以很放心地说,这两个温度值就是一样的――如果它们不一样,那也是温度传感器的问题。你根本不需要一秒钟以内的准确性,那你为什么要去保证写入的互斥?

那又怎么样呢?无非是用下一秒钟的温度冲掉了上一秒的而已。

---------------------------
觉得你潜意识里仍然认为put是顺序执行的,真的是下冲掉上一次么
不是的,是两次并发执行,有可能是第一个long的几个字节和第二个long里面的几个字节组合成一个从来没有出现过的数据,
这是写写冲突,同理如果读了几个字节,写线程写入,读线程读取剩下的,读出的数据肯定不是其中的任何一个,


也许用温度的例子不太合适,OK,你也说了会发生并发冲突,

并发冲突了又怎么样呢?有什么严重的后果?
----------------
如果没后果我们也不用费劲搞什么同步互斥了

如果put 的是对象,可能这个对象二进制代码回是两个对象的混合体,结果谁也不认识,这还不严重么

>>不是的,是两次并发执行,有可能是第一个long的几个字节和第二个long里面的几个字节组合成一个从来没有出现过的数据

这不是滑天下之大稽吗?你放进map的是什么?是一个对象的句柄。难道对象句柄会因为缺乏同步而变成无效的?那你应该去提交JDK的bug,而不是在这个论坛提这种问题。

你的意思是hashmap的value存取是线程安全的了?

> 你的意思是hashmap的value存取是线程安全的了?

当然不是,JDK文档写得清清楚楚,HashMap不是线程安全的,HashTable才是。但请你搞搞清楚,“线程不安全”究竟是什么意思。它的意思是,如果两个写操作并发进去了,你将无法确保两个操作结束后得到的是后一次操作的结果。Java有可能像你说的那样,因为并发问题把对象句柄搞坏掉吗?32位整数操作天生是原子的,这在Java语言规范里面写得清清楚楚,你完全可以先看了再说,做技术的那么多臆测不是好事。

Ok 引用的问题是我的疏忽,可是你也说了hashmap不是线程安全的,你的put(key,value)是安全的么?答案是未必,也就是说put未必是32位操作,这你同意吧?jdk文档上都有

那么还是那个问题,你的put(city,long)是不安全的,因为city不一定存在,所以还是要保持读写,写写互斥吧?

我的问题是这样的:如果要保持写写,读写互斥,但不要读读互斥,hastable 不符合要求 hashma有什么办法让他可以符合这个要求?

>> 可是你也说了hashmap不是线程安全的,你的put(key,value)是安全的么?答案是未必,也就是说put未必是32位操作,这你同意吧?jdk文档上都有

我发现很难跟你沟通,因为你连基本的常识都还缺乏。我已经说过了,HashMap的“线程不安全性”是指:如果有两个并发写操作进入,不能保证整体的结果是后一操作的结果。譬如一个空的HashMap,如果两个并发调用下列语句:


//假设下列两个语句并发调用
map.put("key", "value1");
map.put("key", "value2");

将无法保证map.get("key")得到"value1"或是"value2",这是线程不安全的意义。至于你担心的“对象句柄被破坏”的问题,我请问你,put方法做什么事?不过是把一个对象的引用放到map分配的空间里而已。对象的引用是什么?不就是一个32位整数吗?如果说Java连并发环境下的对象句柄操作都不能保证完整性,它还算哪门子的“高级语言”?你还真把James Gosling当白痴啊?

下次请先补好了基础再来谈论应用问题,不要在讨论过程中还要不停地给你补习这些大学本科一年级的常识,这种讨论太累。

有话儿好好说有话儿好好说有话儿好好说

从讨论中我还真有收获耶,赫赫……