单例对象(Singleton)是一种经常使用的设计模式。在Java应用中,单例对象能保证在一个JVM中,该对象只有一个实例存在。正是因为这个特 点,单例对象一般做为程序中的存放配置信息的载体,由于它能保证其余对象读到一致的信息。例如在某个服务器程序中,该服务器的配置信息可能存放在数据库或 文件中,这些配置数据由某个单例对象统一读取,服务进程中的其余对象若是要获取这些配置信息,只需访问该单例对象便可。这种方式极大地简化了在复杂环境 下,尤为是多线程环境下的配置管理,可是随着应用场景的不一样,也可能带来一些同步问题。 本文将探讨一下在多线程环境下,使用单例对象做配置信息管理时可能会带来的几个同步问题,并针对每一个问题给出可选的解决办法。 问题描述 在多线程环境下,单例对象的同步问题主要体如今两个方面,单例对象的初始化和单例对象的属性更新。 本文描述的方法有以下假设: 1. 单例对象的属性(或成员变量)的获取,是经过单例对象的初始化实现的。也就是说,在单例对象初始化时,会从文件或数据库中读取最新的配置信息。 2. 其余对象不能直接改变单例对象的属性,单例对象属性的变化来源于配置文件或配置数据库数据的变化。 1.1 单例对象的初始化 首先,讨论一下单例对象的初始化同步。单例模式的一般处理方式是,在对象中有一个静态成员变量,其类型就是单例类型自己;若是该变量为null,则建立该单例类型的对象,并将该变量指向这个对象;若是该变量不为null,则直接使用该变量。 其过程以下面代码所示: Java代码 收藏代码 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中有多个单例类型的实例。若是这个单例类型的成员变量在运行过程当中变化,会 形成多个单例类型实例的不一致,产生一些很奇怪的现象。例如,某服务进程经过检查单例对象的某个属性来中止多个线程服务,若是存在多个单例对象的实例,就 会形成部分线程服务中止,部分线程服务不能中止的状况。 1.2 单例对象的属性更新 一般,为了实现配置信息的实时更新,会有一个线程不停检测配置文件或配置数据库的内容,一旦发现变化,就更新到单例对象的属性中。在更新这些信 息的时候,极可能还会有其余线程正在读取这些信息,形成意想不到的后果。仍是以经过单例对象属性中止线程服务为例,若是更新属性时读写不一样步,可能访问该 属性时这个属性正好为空(null),程序就会抛出异常。 解决方法 2.1 单例对象的初始化同步 对于初始化的同步,能够经过以下代码所采用的方式解决。 Java代码 收藏代码 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; } } 这种处理方式虽然引入了同步代码,可是由于这段同步代码只会在最开始的时候执行一次或屡次,因此对整个系统的性能不会有影响。 2.2 单例对象的属性更新同步 为了解决第2个问题,有两种方法: 1,参照读者/写者的处理方式 设置一个读计数器,每次读取配置信息前,将计数器加1,读完后将计数器减1.只有在读计数器为0时,才能更新数据,同时要阻塞全部读属性的调用。代码以下。 Java代码 收藏代码 public class GlobalConfig { private static GlobalConfig instance; private Vector properties = null; private boolean isUpdating = false; private int readCount = 0; 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 > 0) { 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; } } 2,采用"影子实例"的办法 具体说,就是在更新属性时,直接生成另外一个单例对象实例,这个新生成的单例对象实例将从数据库或文件中读取最新的配置信息;而后将这些配置信息直接赋值给旧单例对象的属性。以下面代码所示。 Java代码 收藏代码 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 = shadow.getProperties(); } } 注意:在更新方法中,经过生成新的GlobalConfig的实例,从文件或数据库中获得最新配置信息,并存放到properties属性中。 上面两个方法比较起来,第二个方法更好,首先,编程更简单;其次,没有那么多的同步操做,对性能的影响也不大。