单例对象(Singleton)是一种常用的设计模式在Java应用中单例对象能保证在一个JVM中该对象只有一个实例存在正是由于这个特点单例对象通常作为程序中的存放配置信息的载体因为它能保证其他对象读到一致的信息例如在某个服务器程序中该服务器的配置信息可能存放在数据库或文件中这些配置数据由某个单例对象统一读取服务进程中的其他对象如果要获取这些配置信息只需访问该单例对象即可这种方式极大地简化了在复杂环境下尤其是多线程环境下的配置管理但是随着应用场景的不同也可能带来一些同步问题
本文将探讨一下在多线程环境下使用单例对象作配置信息管理时可能会带来的几个同步问题并针对每个问题给出可选的解决办法
问题描述
在多线程环境下单例对象的同步问题主要体现在两个方面单例对象的初始化和单例对象的属性更新
本文描述的方法有如下假设
单例对象的属性(或成员变量)的获取是通过单例对象的初始化实现的也就是说在单例对象初始化时会从文件或数据库中读取最新的配置信息
其他对象不能直接改变单例对象的属性单例对象属性的变化来源于配置文件或配置数据库数据的变化
单例对象的初始化
首先讨论一下单例对象的初始化同步单例模式的通常处理方式是在对象中有一个静态成员变量其类型就是单例类型本身如果该变量为null则创建该单例类型的对象并将该变量指向这个对象如果该变量不为null则直接使用该变量
其过程如下面代码所示
public class GlobalConfig {
private static GlobalConfig instance = null;
private Vector properties = null;
private GlobalConfig() {
//Load configuration information from DB or file
//Set values for properties
}
public static GlobalConfig getInstance() {
if (instance == null) {
instance = new GlobalConfig();
}
return instance;
}
public Vector getProperties() {
return properties;
}
}
这种处理方式在单线程的模式下可以很好的运行但是在多线程模式下可能产生问题如果第一个线程发现成员变量为null准备创建对象这是第二个线程同时也发现成员变量为null也会创建新对象这就会造成在一个JVM中有多个单例类型的实例如果这个单例类型的成员变量在运行过程中变化会造成多个单例类型实例的不一致产生一些很奇怪的现象例如某服务进程通过检查单例对象的某个属性来停止多个线程服务如果存在多个单例对象的实例就会造成部分线程服务停止部分线程服务不能停止的情况
单例对象的属性更新
通常为了实现配置信息的实时更新会有一个线程不停检测配置文件或配置数据库的内容一旦发现变化就更新到单例对象的属性中在更新这些信息的时候很可能还会有其他线程正在读取这些信息造成意想不到的后果还是以通过单例对象属性停止线程服务为例如果更新属性时读写不同步可能访问该属性时这个属性正好为空(null)程序就会抛出异常
解决方法
单例对象的初始化同步
对于初始化的同步可以通过如下代码所采用的方式解决
public class GlobalConfig {
private static GlobalConfig instance = null;
private Vector properties = null;
private GlobalConfig() {
//Load configuration information from DB or file
//Set values for properties
}
private static synchronized void syncInit() {
if (instance == null) {
instance = new GlobalConfig();
}
}
public static GlobalConfig getInstance() {
if (instance == null) {
syncInit();
}
return instance;
}
public Vector getProperties() {
return properties;
}
}
这种处理方式虽然引入了同步代码但是因为这段同步代码只会在最开始的时候执行一次或多次所以对整个系统的性能不会有影响
单例对象的属性更新同步
为了解决第个问题有两种方法
参照读者/写者的处理方式
设置一个读计数器每次读取配置信息前将计数器加读完后将计数器减只有在读计数器为时才能更新数据同时要阻塞所有读属性的调用代码如下
public class GlobalConfig {
private static GlobalConfig instance;
private Vector properties = null;
private boolean isUpdating = false;
private int readCount = ;
private GlobalConfig() {
//Load configuration information from DB or file
//Set values for properties
}
private static synchronized void syncInit() {
if (instance == null) {
instance = new GlobalConfig();
}
}
public static GlobalConfig getInstance() {
if (instance==null) {
syncInit();
}
return instance;
}
public synchronized void update(String p_data) {
syncUpdateIn();
//Update properties
}
private synchronized void syncUpdateIn() {
while (readCount > ) {
try {
wait();
} catch (Exception e) {
}
}
}
private synchronized void syncReadIn() {
readCount++;
}
private synchronized void syncReadOut() {
readCount;
notifyAll();
}
public Vector getProperties() {
syncReadIn();
//Process data
syncReadOut();
return properties;
}
}
采用影子实例的办法
具体说就是在更新属性时直接生成另一个单例对象实例这个新生成的单例对象实例将从数据库或文件中读取最新的配置信息然后将这些配置信息直接赋值给旧单例对象的属性如下面代码所示
public class GlobalConfig {
private static GlobalConfig instance = null;
private Vector properties = null;
private GlobalConfig() {
//Load configuration information from DB or file
//Set values for properties
}
private static synchronized void syncInit() {
if (instance = null) {
instance = new GlobalConfig();
}
}
public static GlobalConfig getInstance() {
if (instance = null) {
syncInit();
}
return instance;
}
public Vector getProperties() {
return properties;
}
public void updateProperties() {
//Load updated configuration information by new a GlobalConfig object
GlobalConfig shadow = new GlobalConfig();
properties = shadowgetProperties();
}
}
注意在更新方法中通过生成新的GlobalConfig的实例从文件或数据库中得到最新配置信息并存放到properties属性中
上面两个方法比较起来第二个方法更好首先编程更简单其次没有那么多的同步操作对性能的影响也不大