Spring Bean 单例模式

1. Refer to <<Spring in action>>, page 39.




2. Spring Bean Creation is Not Thread Safe

refer to http://blog.imaginea.com/spring-bean-creation-is-not-thread-safe/


When it comes to Java Concurrency no body[except Doug Lea] is sure of correctness of the code. The major problem faced by developers is debugging concurrent bugs. They are not reproducible easily and not debuggable by Remote Java Debug as that can effect the behavior of concurrent program.

In this article I am going to talk about a concurrency issue I faced in Spring. I will run through the steps I used for analyzing it. The conclusion would talk about a thumb rule that we may consider while refactoring concurrent code.

Issue: While using Spring as a bean container in heavy load I was getting some beans which were not initialized completely.

Approach

1. Divide: The first step was to isolate the problem to its smallest set of replication steps. This is real important as unless you can point to very small piece of code which has issue, it will be hard to debug it. So I created two classes say A and B. In Class A I was injecting an instance of B. My Classes looked something like this:

public class A{private B b;//getters and setters for b}public class B{}

I configured these beans in spring as lazy. I then started 1000 threads and all threads invoked getBean on Spring ApplicationContext. Many of the threads got instance of Class “A” with instance variable “b” being null, which means Spring gave me a bean instance which was not fully initialized.

2. Gather: The next step was to gather more information about the variable states. Unfortunately, debugging for concurrency issues has its drawbacks, so I configured logging for Spring.

3. Analyze: I found following statements in log

beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean ‘A’

beans.factory.support.DefaultListableBeanFactory - Eagerly caching bean 'A' to allow for resolving potential circular references


beans.factory.support.DefaultListableBeanFactory - Returning eagerly cached instance of singleton bean 'A' that is not fully initialized yet - a consequence of a circular reference
beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'B'

4. So just from going through the logs I got a hint that something is wrong with eager caching of Beans.

Lets dive into Spring Code Base and see why i was getting uninitialized Bean “A”
Lets look into AbstractBeanFactory method getBean
Class : org.springframework.beans.factory.support.AbstractBeanFactory
Method : getBean
Code Snippet :

Object sharedInstance = getSingleton(beanName);     if (sharedInstance != null) {       if (logger.isDebugEnabled()) {             if (isSingletonCurrentlyInCreation(beanName)) {                   logger.debug("Returning eagerly cached instance of singleton bean '" + beanName +                   "that is not fully initialized yet - a consequence of a circular reference");               }               else {                   logger.debug("Returning cached instance of singleton bean " + beanName);              }      }     bean = getObjectForBeanInstance(sharedInstance, name, beanName);}

This means Bean A was identified as an eagerly cached instance. Seems like, an eagerly cached instance can be returned before it is completely initialized. Though the code seemed to expect such scenarios only in case of circular references, this certainly wasn’t true in my case [as I don’t have any circular reference in my bean definition].

Lets look at the Locks used in this method:
a.) There is no lock in the method shown above
b.) There are no locks in getSingleton either, this method looks as follows:

public Object getSingleton(String beanName) {      Object singletonObject = this.singletonObjects.get(beanName);      return (singletonObject != NULL_OBJECT ? singletonObject : null);}

there are no explicit locks, only lock taken is in get method on the map singletonObject which happens to be a concurrent Hash Map.

The surprising part is the absence of locks in getSingleton method, a lock here could have stopped cached beans from returning to other threads [as circular reference call will come from same Thread.]?
So is there an issue with locking in getSingleton method?
A possible sequence of steps:
Thread 1: Goes and asks for bean “A”. Spring starts instantiating the bean. Adds it to the singletonObjects map before completely resolving all dependencies of bean.
Thread 2: Goes and asks for bean “A” and till this time Thread 1 has already added it into singletonObjects map but Thread 1 is still resolving dependencies. But now Thread 2 will see an instance of “A” which is not completely initialized.

That’s why Spring Bean Creation is not Thread Safe.

If you dig further you will see that Bean Creation in Spring is guraded by a lock on singletonObjects, but method getSingleton(String beanName) is not guarded by the same lock.
Bean Creation Method in Spring:

public Object getSingleton(String beanName, ObjectFactory singletonFactory) {    Assert.notNull(beanName, "'beanName' must not be null");    synchronized (this.singletonObjects) {

So you can see that Bean Creation is guarded by lock on singletonObjects but getSingleton(String beanName) is not guarded by same lock.
How to Fix it.

public Object getSingleton(String beanName) {     synchronized(this.singletonObjects){           Object singletonObject = this.singletonObjects.get(beanName);           return (singletonObject != NULL_OBJECT ? singletonObject : null);     } }

Now getSingleton(String beanName) and method which creates the Bean both are guarded by the same lock. So now Bean Creation is Thread Safe.

You will find this bug in Spring Version 2.5 and not in 2.0. Why ?
Lets look at Spring 2.0 code :
Class : DefaultSingletonBeanRegistry

Class : DefaultSingletonBeanRegistry public Object getSingleton(String beanName) {      synchronized (this.singletonCache) {         return this.singletonCache.get(beanName);     } }public Object getSingleton(String beanName, ObjectFactory singletonFactory) {     synchronized (this.singletonCache) {     ......

So it works as in this case as both methods are guarded by the same lock.But why in the 2.5 version this thing was not taken care of.
If you look in 2.0 code you will find:

/** Cache of singleton objects: bean name –> bean instance */

private final Map singletonCache = new HashMap();

And in 2.5 code :

/** Cache of singleton objects: bean name –> bean instance */

private final Map singletonObjects = CollectionFactory.createConcurrentMapIfPossible(16);

As can be seen the map used in 2.0 code base was a HashMap and in 2.5 method it was replaced with a ThreadSafe Map.
So the author might have thought he doesn’t need any other lock in :

public Object getSingleton(String beanName) {   Object singletonObject = this.singletonObjects.get(beanName);   return (singletonObject != NULL_OBJECT ? singletonObject : null);}

Because the singleton map was now (in 2.5) Thread Safe.

This is one of the reasons why we should be careful while refactoring concurrent code and we should make sure Locks are same.

And specifically while moving from HashMap to a Thread Safe Map, check earlier lock taken was not guarding other then making the Map Thread Safe. Like in this case Lock was guarding singleton cache also and Map thread safety also.


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Spring中,Bean的默认作用域是单例模式,即每个容器只会创建一个实例。对于单例模式Bean,如果多个线程同时访问该Bean的方法或属性,可能会引发线程安全问题。这是因为多个线程共享同一个实例,对实例的修改可能会相互干扰。因此,需要特别注意在单例模式下处理线程安全问题。 对于解决单例模式下的线程安全问题,可以采取以下几种方式: 1. 方法级别加锁:在需要保证线程安全的方法上使用synchronized关键字,确保同一时间只能有一个线程访问该方法。这种方式简单直接,但会降低并发性能。 2. 使用ThreadLocal:ThreadLocal是Java提供的一种线程级别的变量隔离机制。可以将需要共享的对象存储在ThreadLocal中,每个线程都拥有自己的副本,避免了线程安全问题。可以将非线程安全的对象存储在ThreadLocal中,确保每个线程都使用自己的对象副本。 3. 将Bean的作用域设置为prototype:使用prototype作用域的Bean在每次被注入或获取时都会创建一个新的实例,解决了单例模式下的线程安全问题。可以通过在Bean的配置文件中设置scope属性为"prototype"来实现。 总结起来,为了解决Spring Bean单例模式下的线程安全问题,可以采用方法级别加锁、使用ThreadLocal或将Bean的作用域设置为prototype等方式来保证线程安全性。具体选择哪种方式需要根据实际场景和需求来决定。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值