单例对象.docx
《单例对象.docx》由会员分享,可在线阅读,更多相关《单例对象.docx(16页珍藏版)》请在冰豆网上搜索。
单例对象
单例对象(Singleton)是一种常用的设计模式。
在Java应用中,单例对象能保证在一个JVM中,该对象只有一个实例存在。
正是由于这个特点,单例对象通常作为程序中的存放配置信息的载体,因为它能保证其他对象读到一致的信息。
例如在某个服务器程序中,该服务器的配置信息可能存放在数据库或文件中,这些配置数据由某个单例对象统一读取,服务进程中的其他对象如果要获取这些配置信息,只需访问该单例对象即可。
这种方式极大地简化了在复杂环境下,尤其是多线程环境下的配置管理,但是随着应用场景的不同,也可能带来一些同步问题。
本文将探讨一下在多线程环境下,使用单例对象作配置信息管理时可能会带来的几个同步问题,并针对每个问题给出可选的解决办法。
问题描述
在多线程环境下,单例对象的同步问题主要体现在两个方面,单例对象的初始化和单例对象的属性更新。
本文描述的方法有如下假设:
1.单例对象的属性(或成员变量)的获取,是通过单例对象的初始化实现的。
也就是说,在单例对象初始化时,会从文件或数据库中读取最新的配置信息。
2.其他对象不能直接改变单例对象的属性,单例对象属性的变化来源于配置文件或配置数据库数据的变化。
1.1单例对象的初始化
首先,讨论一下单例对象的初始化同步。
单例模式的通常处理方式是,在对象中有一个静态成员变量,其类型就是单例类型本身;如果该变量为null,则创建该单例类型的对象,并将该变量指向这个对象;如果该变量不为null,则直接使用该变量。
其过程如下面代码所示:
Java代码
1publicclassGlobalConfig{
2privatestaticGlobalConfiginstance=null;
3privateVectorproperties=null;
4privateGlobalConfig(){
5//LoadconfigurationinformationfromDBorfile
6//Setvaluesforproperties
7}
8publicstaticGlobalConfiggetInstance(){
9if(instance==null){
10instance=newGlobalConfig();
11}
12returninstance;
13}
14publicVectorgetProperties(){
15returnproperties;
16}
17}
这种处理方式在单线程的模式下可以很好的运行;但是在多线程模式下,可能产生问题。
如果第一个线程发现成员变量为null,准备创建对象;这是第二个线程同时也发现成员变量为null,也会创建新对象。
这就会造成在一个JVM中有多个单例类型的实例。
如果这个单例类型的成员变量在运行过程中变化,会造成多个单例类型实例的不一致,产生一些很奇怪的现象。
例如,某服务进程通过检查单例对象的某个属性来停止多个线程服务,如果存在多个单例对象的实例,就会造成部分线程服务停止,部分线程服务不能停止的情况。
1.2单例对象的属性更新
通常,为了实现配置信息的实时更新,会有一个线程不停检测配置文件或配置数据库的内容,一旦发现变化,就更新到单例对象的属性中。
在更新这些信息的时候,很可能还会有其他线程正在读取这些信息,造成意想不到的后果。
还是以通过单例对象属性停止线程服务为例,如果更新属性时读写不同步,可能访问该属性时这个属性正好为空(null),程序就会抛出异常。
解决方法
2.1单例对象的初始化同步
对于初始化的同步,可以通过如下代码所采用的方式解决。
Java代码
18publicclassGlobalConfig{
19privatestaticGlobalConfiginstance=null;
20privateVectorproperties=null;
21privateGlobalConfig(){
22//LoadconfigurationinformationfromDBorfile
23//Setvaluesforproperties
24}
25privatestaticsynchronizedvoidsyncInit(){
26if(instance==null){
27instance=newGlobalConfig();
28}
29}
30publicstaticGlobalConfiggetInstance(){
31if(instance==null){
32syncInit();
33}
34returninstance;
35}
36publicVectorgetProperties(){
37returnproperties;
38}
39}
这种处理方式虽然引入了同步代码,但是因为这段同步代码只会在最开始的时候执行一次或多次,所以对整个系统的性能不会有影响。
2.2单例对象的属性更新同步
为了解决第2个问题,有两种方法:
1,参照读者/写者的处理方式
设置一个读计数器,每次读取配置信息前,将计数器加1,读完后将计数器减1.只有在读计数器为0时,才能更新数据,同时要阻塞所有读属性的调用。
代码如下。
Java代码
40publicclassGlobalConfig{
41privatestaticGlobalConfiginstance;
42privateVectorproperties=null;
43privatebooleanisUpdating=false;
44privateintreadCount=0;
45privateGlobalConfig(){
46//LoadconfigurationinformationfromDBorfile
47//Setvaluesforproperties
48}
49privatestaticsynchronizedvoidsyncInit(){
50if(instance==null){
51instance=newGlobalConfig();
52}
53}
54publicstaticGlobalConfiggetInstance(){
55if(instance==null){
56syncInit();
57}
58returninstance;
59}
60publicsynchronizedvoidupdate(Stringp_data){
61syncUpdateIn();
62//Updateproperties
63}
64privatesynchronizedvoidsyncUpdateIn(){
65while(readCount>0){
66try{
67wait();
68}catch(Exceptione){
69}
70}
71}
72privatesynchronizedvoidsyncReadIn(){
73readCount++;
74}
75privatesynchronizedvoidsyncReadOut(){
76readCount--;
77notifyAll();
78}
79publicVectorgetProperties(){
80syncReadIn();
81//Processdata
82syncReadOut();
83returnproperties;
84}
85}
2,采用"影子实例"的办法
具体说,就是在更新属性时,直接生成另一个单例对象实例,这个新生成的单例对象实例将从数据库或文件中读取最新的配置信息;然后将这些配置信息直接赋值给旧单例对象的属性。
如下面代码所示。
Java代码
86publicclassGlobalConfig{
87privatestaticGlobalConfiginstance=null;
88privateVectorproperties=null;
89privateGlobalConfig(){
90//LoadconfigurationinformationfromDBorfile
91//Setvaluesforproperties
92}
93privatestaticsynchronizedvoidsyncInit(){
94if(instance=null){
95instance=newGlobalConfig();
96}
97}
98publicstaticGlobalConfiggetInstance(){
99if(instance=null){
100syncInit();
101}
102returninstance;
103}
104publicVectorgetProperties(){
105returnproperties;
106}
107publicvoidupdateProperties(){
108//LoadupdatedconfigurationinformationbynewaGlobalConfigobject
109GlobalConfigshadow=newGlobalConfig();
110properties=shadow.getProperties();
111}
112}
注意:
在更新方法中,通过生成新的GlobalConfig的实例,从文件或数据库中得到最新配置信息,并存放到properties属性中。
上面两个方法比较起来,第二个方法更好,首先,编程更简单;其次,没有那么多的同步操作,对性能的影响也不大。
多线程中单例模式
Abstract
在开发中,如果某个实例的创建需要消耗很多系统资源,那么我们通常会使用惰性加载机制,也就是说只有当使用到这个实例的时候才会创建这个实例,这个好处在单例模式中得到了广泛应用。
这个机制在single-threaded环境下的实现非常简单,然而在multi-threaded环境下却存在隐患。
本文重点介绍惰性加载机制以及其在多线程环境下的使用方法。
(作者numberzero,参考IBM文章《Double-checkedlockingandtheSingletonpattern》,欢迎转载与讨论)
1单例模式的惰性加载
通常当我们设计一个单例类的时候,会在类的内部构造这个类(通过构造函数,或者在定义处直接创建),并对外提供一个staticgetInstance方法提供获取该单例对象的途径。
例如:
Java代码
publicclassSingleton
{
privatestaticSingletoninstance=newSingleton();
privateSingleton(){
…
}
publicstaticSingletongetInstance(){
returninstance;
}
}
publicclassSingleton
{
privatestaticSingletoninstance=newSingleton();
privateSingleton(){
…
}
publicstaticSingletongetInstance(){
returninstance;
}
}
这样的代码缺点是:
第一次加载类的时候会连带着创建Singleton实例,这样的结果与我们所期望的不同,因为创建实例的时候可能并不是我们需要这个实例的时候。
同时如果这个Singleton实例的创建非常消耗系统资源,而应用始终都没有使用Singleton实例,那么创建Singleton消耗的系统资源就被白白浪费了。
为了避免这种情况,我们通常使用惰性加载的机制,也就是在使用的时候才去创建。
以上代码的惰性加载代码如下:
Java代码
publicclassSingleton{
privatestaticSingletoninstance=null;
privateSingleton(){
…
}
publicstaticSingletongetInstance(){
if(instance==null)
instance=newSingleton();
returninstance;
}
}
publicclassSingleton{
privatestaticSingletoninstance=null;
privateSingleton(){
…
}
publicstaticSingletongetInstance(){
if(instance==null)
instance=newSingleton();
returninstance;
}
}
这样,当我们第一次调用Singleton.getInstance()的时候,这个单例才被创建,而以后再次调用的时候仅仅返回这个单例就可以了。
2惰性加载在多线程中的问题
先将惰性加载的代码提取出来:
Java代码
publicstaticSingletongetInstance(){
if(instance==null)
instance=newSingleton();
returninstance;
}
publicstaticSingletongetInstance(){
if(instance==null)
instance=newSingleton();
returninstance;
}这是如果两个线程A和B同时执行了该方法,然后以如下方式执行:
1.A进入if判断,此时foo为null,因此进入if内
2.B进入if判断,此时A还没有创建foo,因此foo也为null,因此B也进入if内
3.A创建了一个Foo并返回
4.B也创建了一个Foo并返回
此时问题出现了,我们的单例被创建了两次,而这并不是我们所期望的。
3各种解决方案及其存在的问题
3.1使用Class锁机制
以上问题最直观的解决办法就是给getInstance方法加上一个synchronize前缀,这样每次只允许一个现成调用getInstance方法:
Java代码
publicstaticsynchronizedSingletongetInstance(){
if(instance==null)
instance=newSingleton();
returninstance;
}
publicstaticsynchronizedSinglet