`

单例对象的线程同步问题

阅读更多

本文转自

http://cncc.bingj.com/cache.aspx?q=Java++static%e5%b1%9e%e6%80%a7%e5%90%8c%e6%ad%a5&d=4759042901870046&mkt=zh-CN&setlang=zh-CN&w=a3f86aa2,49503381

 

单例对象(Singleton)是一种常用的设计模式。在Java应用中,单例对象能保证在一个JVM中,该对象只有一个实例存在。正是由于这个特点,单例对象通常作为程序中的存放配置信息的载体,因为它能保证其他对象读到一致的信息。例如在某个服务器程序中,该服务器的配置信息可能存放在数据库或文件中,这些配置数据由某个单例对象统一读取,服务进程中的其他对象如果要获取这些配置信息,只需访问该单例对象即可。这种方式极大地简化了在复杂环境下,尤其是多线程环境下的配置管理,但是随着应用场景的不同,也可能带来一些同步问题。

本文将探讨一下在多线程环境下,使用单例对象作配置信息管理时可能会带来的几个同步问题,并针对每个问题给出可选的解决办法。

问题描述

 

在多线程环境下,单例对象的同步问题主要体现在两个方面,单例对象的初始化和单例对象的属性更新。

本文描述的方法有如下假设:

  1. 单例对象的属性(或成员变量)的获取,是通过单例对象的初始化实现的。也就是说,在单例对象初始化时,会从文件或数据库中读取最新的配置信息。
  2. 其他对象不能直接改变单例对象的属性,单例对象属性的变化来源于配置文件或配置数据库数据的变化。

1.1 单例对象的初始化

 

首先,讨论一下单例对象的初始化同步。单例模式的通常处理方式是,在对象中有一个静态成员变量,其类型就是单例类型本身;如果该变量为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中有多个单例类型的实例。如果这个单例类型的成员变量在运行过程中变化,会造成多个单例类型实例的不一致,产生一些很奇怪的现象。例如,某服务进程通过检查单例对象的某个属性来停止多个线程服务,如果存在多个单例对象的实例,就会造成部分线程服务停止,部分线程服务不能停止的情况。

1.2 单例对象的属性更新

 

通常,为了实现配置信息的实时更新,会有一个线程不停检测配置文件或配置数据库的内容,一旦发现变化,就更新到单例对象的属性中。在更新这些信息的时候,很可能还会有其他线程正在读取这些信息,造成意想不到的后果。还是以通过单例对象属性停止线程服务为例,如果更新属性时读写不同步,可能访问该属性时这个属性正好为空(null),程序就会抛出异常。

 




回页首


解决方法

 

2.1 单例对象的初始化同步

 

对于初始化的同步,可以通过如下代码所采用的方式解决。

  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时,才能更新数据,同时要阻塞所有读属性的调用。代码如下。

  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,采用"影子实例"的办法

具体说,就是在更新属性时,直接生成另一个单例对象实例,这个新生成的单例对象实例将从数据库或文件中读取最新的配置信息;然后将这些配置信息直接赋值给旧单例对象的属性。如下面代码所示。

  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属性中。

上面两个方法比较起来,第二个方法更好,首先,编程更简单;其次,没有那么多的同步操作,对性能的影响也不大。


关于作者

 

邓明:IBM中国有限公司 BCS 高级信息系统工程师。

分享到:
评论

相关推荐

    浅议单例模式之线程安全(转)

    在懒汉式的基础上添加了同步锁,避免了线程安全问题。 ```java public class Singleton { private volatile static Singleton INSTANCE; private Singleton() {} public static Singleton getInstance() {...

    Qt两种方法实现多线程并安全结束线程及QMutex加锁Qt单例化实现

    - **Q_GLOBAL_STATIC宏**:Qt提供了一个更方便的宏`Q_GLOBAL_STATIC`来创建全局单例对象,避免了传统单例模式的内存泄漏问题。 - **示例**:定义一个单例类,包含静态成员函数`getInstance()`返回单例对象,使用`Q...

    线程安全的单例模式

    懒汉式单例解决了饿汉式单例在未使用时即占用资源的问题,采用延迟加载机制,在第一次调用 `getSingleInstance()` 方法时才创建对象。然而,这种方式存在线程安全问题,当多个线程几乎同时调用 `getSingleInstance...

    c++单例模式线程日志类

    为了确保线程安全,日志类可能使用互斥量(mutex)或锁(lock)等同步原语来保证在同一时刻只有一个线程可以写入日志。这样可以避免并发问题,确保日志的正确性和完整性。 日志等级是另一个关键特性,它允许开发者...

    synchronized与单例的线程安全

    "synchronized"关键字和单例模式是确保线程安全的两种常见手段。本文将详细探讨这两个概念及其在实现线程安全中的作用。 一、synchronized关键字 synchronized是Java中的一个关键同步机制,用于控制对类或对象的...

    spring单例引起的线程安全问题

    总结来说,Spring 中的单例模式虽然方便了对象管理,但如果不注意线程安全问题,可能会导致意料之外的结果。在设计类时,应该根据业务需求谨慎选择实例化策略,并且对于可变状态的单例类,应采取适当的同步措施或改...

    unity中涉及的三种单例模式

    在这个版本中,当单例对象被创建后,会调用`DontDestroyOnLoad`确保它不会随场景改变而消失。 3. 线程安全的单例模式: 在多线程环境下,C#经典单例可能存在问题。为了确保在任何时刻只有一个线程能执行创建单例的...

    线程安全单例

    对于那些需要频繁访问单例对象但又希望保证线程安全性的应用场景来说,这种实现方式无疑是一个优秀的选择。开发者在实际项目中可以根据具体需求选择合适的单例实现方式,但“静态内部类法”因其简洁高效而备受推崇。

    使用C++11实现线程安全的单例模式

    C++11引入了 `<thread>` 头文件,提供了线程创建、同步等机制,使开发者能够编写多线程程序。`std::mutex` 是一个互斥锁,用于保护共享资源,确保同一时间只有一个线程可以访问;`std::call_once` 是一种保证某个...

    多线程单例模式并发访问

    - **饿汉式**:在类装载时即完成初始化,避免了线程同步的问题。 - **懒汉式**:通过静态内部类或者双重检查锁定的方式来实现线程安全的懒加载单例模式,即在第一次使用时才初始化。 #### 十、线程安全的懒汉式单例...

    27_多线程_第2天(线程安全、线程同步、等待唤醒机制、单例设计模式)_讲义.doc

    在Java中,如果多个线程共享同一份全局变量或静态变量,且这些线程中有写操作,不进行线程同步可能会引发线程安全问题。例如,在电影院售票的例子中,如果没有同步机制,可能出现票号重复或者负数的情况。 【线程...

    线程相关的单例模式

    在多线程环境下,如果不加以同步控制,可能会出现多个线程同时创建单例对象的情况。为了解决这个问题,可以使用双重检查锁定(Double-Checked Locking,DCL)机制,它在保证线程安全的同时,尽可能地减少了同步的...

    27_多线程_第2天(线程安全、线程同步、等待唤醒机制、单例设计模式)_讲义

    然而,多线程环境下也会带来一系列问题,如数据不一致性、资源竞争等,因此,理解线程安全、线程同步以及等待唤醒机制至关重要。此外,单例设计模式在确保一个类只有一个实例的同时,提供了全局访问点,也是多线程...

    线程同步的理解

    线程同步是多线程编程中的一个重要概念,用于解决多个线程并发访问共享资源时可能出现的数据不一致性问题。在Java中,线程同步主要通过关键字`synchronized`来实现,它可以确保在同一时间只有一个线程能够执行特定的...

    NET面试题-多线程编程与线程同步1

    - GUI控件通常由主线程创建和管理,不支持跨线程访问,以防止数据竞争和同步问题。解决办法通常使用控件的Invoke或BeginInvoke方法,确保在正确的线程上执行操作。 3. **后台线程与前台线程**: - 前台线程是维持...

    Java多线程-解决单例模式中的懒汉式的线程安全问题

    ### Java多线程—解决单例模式中的懒汉式的线程安全问题 #### 一、单例设计模式的线程安全问题 ##### (1)饿汉式没有线程安全问题 **饿汉式**是一种非常典型的单例模式实现方式,其特点是在类加载时就完成了实例...

    完美解决单例设计模式中懒汉式线程安全的问题

    总结来说,解决懒汉式单例的线程安全问题通常涉及到同步控制,如同步锁机制或双重检查锁定。选择哪种方法取决于性能和线程安全性的权衡。在实际开发中,考虑到性能和代码可读性,更推荐使用双重检查锁定来实现线程...

    Java多线程实战之单例模式与多线程的实例详解

    Java多线程实战之单例模式与多线程的实例详解 单例模式是Java多线程编程中最...Java多线程实战之单例模式与多线程的实例详解中,我们讨论了立即加载/饿汉模式和延迟加载/懒汉模式,并且讨论了如何解决线程安全问题。

    单例模式的四种实现方法

    与懒汉式不同的是,饿汉式在类加载时就完成了初始化,因此没有线程安全问题,但这也意味着单例对象的生命周期和应用程序相同。 ##### 代码示例: ```java public class Singleton { private static Singleton sing...

    单例模式下,使用多线程实现

    4. **饿汉式(即静态初始化)**:在类加载时就完成了实例化,避免了多线程同步问题。但这种方式并不符合"懒初始化"的原则,如果单例类不会被使用,那么内存就会被浪费: ```java public class Singleton { ...

Global site tag (gtag.js) - Google Analytics