Spring and visibility problems

Spring and visibility problems

In Beijing on 2007-7-21

Source: http://blog.xebia.com/2007/03/01/spring-and-visibility-problems/

Spring 是一个优秀的框架,但我希望那些基于Spring的程序不受到可见性问题(一种并发问题)。本文着力与描述产生该问题的原因,以及如何采用一些方法来解决它。

可见性问题

许多程序员不具有很多关于多线程的经验,对它仅有一个简单的看法:如果一个线程修改了一个变量,会使得其他的线程可以直接的获取修改后的值。这个看法被称为连续一致性(sequential consistency),但问题是没有一个虚拟机自动的来实现这个观点。

性能问题是这个显然不完美行为的原因。大多数变量并没有在线程中共享,因此没有特定的指令用来解决连续一致性的问题。如果这些指令被自动的增加被多线程访问的处理,将会被极大的降低性能,因为很多优化处理会被从目前使用的方式中删除。例如:

  1. 使用快速本地存储(例如缓存和寄存器),这样会降低多核系统的性能。因为增加对主存的访问将会成为系统的瓶颈。
  2. reordering of instructions to increase the chance of a cache hit.
    重新对指令进行排序会增加缓存的命中率。

可见性问题的结果是一个变量在一个线程中被修改,不会被其他线程可见(或许永不)。这样会很容易产生一个简单的结果,空指针异常,或者一个很难发现的错误,如下所示:

public class SomeRunnable implements Runnable{

    private boolean stop = false;

 

    public void stop(){

        stop = true;

    }

 

    public void run(){

        while(!stop){

                System.out.println("hello");

        }

    }

}

这里存在几个该线程停止失败的原因:

  1. 变量stop没有正确的被公布:运行run方法的线程没有必要去查看stop变量的修改。可能存在的场景是:线程1(运行run方法)在cpu1中运行(使用缓存1)。线程2(运行停止方法)在cpu2中运行(使用缓存2)。当线程1在运行是,它需要变量stop,获取它的原始值false,并放入缓存中。在线程2执行stop方法时,变量stop被设置为true。但是可能会被存放在缓存2中一个不确定的时间,没有被更新到主存中去,因此线程1永远没有观察到它的修改。或者即使变量的修改被更新到了主存,线程1仍从缓存1中获取失效的变量。
  2. 因为变量的访问没有被安全的公布,并且变量在循环中没有被修改,编译器(例如JIT,或者CPU)可能使用常量值’true’来替换循环中的变量。

可见性问题会导致出现很难检测的错误,和不可预测的行为,因此你需要明确的从你的系统中去除它们。尽管多数CPU不会导致可见性问题(因为主存一致性),我还是打赌这个行为会发生问题(因为应用程序永远不是平台无关)。

Spring 应用和可见性问题

我猜测很多Spring应用都受制与可见性问题。如果你观看一下例子中的单例应用(dao’s,控制器,管理者等),这些beans经常在多线程中共享,例如:

  • servlet容器中的线程
  • 在远程中间件中的线程
  • jmx中的线程
  • 在事件触发或者其他内部的线程

看一下下面的例子:

public class EmployeeManagerImpl implements EmployeeManager{

 

    private EmployeeDao employeeDao;

 

    public setEmployeeDao(EmployeeDao employeeDao){

        this.employeeDao = employeeDao;

    }

 

    public void fire(long employeeId){

        Employee employee = employeeDao.load(employeeId);

        employee.fire();

    }

}

上面演示bean的配置文件如下:

    <bean id="employeeManager" class="EmployeeManagerImpl">

        <property name="employeeDao" ref="employeeDao"/>

    </bean>

这里的可见性问题是employeeDao对象,通过employeeManager的构建器线程,没有被安全的发布,而不可被其他线程观察到。如果一个这样的线程在EmployeeManagerImpl类中调用fire方法,将会导致空指针异常,这个线程获取的还是一个空值的employeeDao

这个问题可以通过以下方法解决:

  1. 使用volatile修饰employeeDao变量。尽管它看起来很奇怪,因为employeeDao在设置后不会再发生变化,volatile类型的变量总是可以被正确的发布。我个人的观点是不太赞同这个解决方法,因为这样会产生误导(变量值再对象构建后不会再发生变化),而且这样做也会降低性能(变量总是会强制从主存中读取,而不是使用缓存方式)。
  2. 使用final修饰employeeDao变量,因为常量(final variables)方式也总是可以正确地被发布。使用常量方式的问题是只能够使用构造器方式来设置依赖注入(Dependency Injection),而Spring则推荐使用setter方式注入。我个人的观点是更喜欢采用构造器方式,这样可以更简单的保证类的一致性。尽管使用setter方式仅存在与其实现的类上面,而不是在接口上,我还是感到不方便,因为setter方式会使得类变得很难理解。但是构造器方式也不是十分完美,我猜想我们大多都会同很大的构造器进行过斗争。

问题还没有那么坏?

在更详细解释问题还没有那么坏之前,我需要解释一下在java5或者更高版本中新的内存模型(JSR-133)(参看脚注)。新模型描述了什么条件下变量修改会对其他线程可见,和保护推理缓存(它们没有在新的Java内存模型中被提及)。

模型被描述为以下操作类型:

  1. 读、写操作对于普通(normal)、飞逝(volatile)、最终(final)变量
  2. 锁的请求、释放
  3. 线程的启动、连接

模型也包含着一套在操作中的发生前(happens-before)规则。如果一个发生前规则被应用与操作1和操作2中间,则所有被操作1修改的操作都会被操作2所可见。发生前规则例子如下:

  1. 程序顺序规则:每一个操作均在一个线程中发生,每一个操作以程序顺序的方式发生。对于一般变量的读、写操作很重要,只要保持在一个线程内、在一个语意内,可以被重新排序:重新排序在线程内不可见。
  2. 飞逝性变量规则:一个对飞逝性变量的修改,后续的读操作会获取相同的值(这就是为什么我们修改employeeDao变量为飞逝性的原因)。
  3. 监控锁规则:对一个监视器的释放会在后续的所有获取监控器锁之前(可以参考Java5中的新锁实现)。
  4. 传递规则:如果操作1在操作2前,操作2在操作3前,那么操作1则会在操作3之前。

如果存在对一个变量的写入、读取的发生前关系,那么它可以被正确的发布。

安全传递

这些发生前规则可以用来检测两个操作之间是否存在发生前关系。看一下下面的例子:

int a=0;

volatile boolean b=0;

 

void initialize(){

        a=console.readInteger();

        b=console.readBoolean();

}

 

void print(){

        print(b);

        print(a);

}

可能存在的操作顺序为:

action

thread 1

thread 2

action1

normalwrite(a)

 

action2

volatilewrite(b)

 

action3

 

volatileread(b)

action4

 

normalread(a)

因为操作1发生在操作2前(程序顺序),而且操作2发生在操作3之前(飞逝性变量规则),操作3发生在操作4之前(程序顺序)。操作1则发生在操作4之前(还记得传递规则么)。这就意味着在操作1中的修改,对于操作4来说是可见的。这个技术被成为“安全传递”。安全传递使用安全发布的变量X(在本例中是变量b),被安全的发布了其他没有安全发布特性的变量在X前(在本例中是a)。

Spring中安全传递

Spring应用上下文中也提供了安全传递(仅在Java5和更高的版本上):在一个单例bean创建后,它被存入一个单例映射中。在它需要使用的时候,从这个映射中获取。但并非应用了“飞逝性变量规则,它使用“监控器锁”规则(参见下面的操作4和操作5)。

下面的表格简单的列出了操作的顺序(变量mapentry.ref就是安全传递技术中的变量’X’):

action

thread 1

thread 2

action1

lock(singletonmap)

 

 

construction of the employeeManager

 

action2

normalwrite(employeeDao)

 

 

employeeManager is placed in the applicationcontext

 

action3

normalwrite(mapentry.ref)

 

action4

unlock(singletonmap)

 

 

 

employeeManager is read from the applicationcontext

action5

 

lock(singletonmap)

action6

 

normalread(mapentry.ref)

action7

 

unlock(singletonmap)

 

 

employeeManager fire method is called

action8

 

normalread(employeeDao)

在操作2和操作8中间存在着发生前关系,因此变量employeeDao被赋值(操作2),对于另外一个线程是可视的(操作8)。这就是为什么标准的Spring单例模式不存在可见性问题。如果你想更详细的了解,可以参看DefaultSingletonBeanRegistry的实现,其中的‘public Object getSingleton(String beanName, ObjectFactory singletonFactory)’的方法。

总结

我很高兴标准的单例bean没有受制与可见性问题(仅在Java5和更高版本中),但是我并不认为这是一个很好的解决方案对于Spring(对于其它也一样)。

  1. 对象还是不能在不同的环境中被安全的使用。因此你的对象被限制在(当前的版本)Spring框架中,才可以正确的实现。
  2. 保存处理的安全传递没有在Spring实现,并且只存在与Java5和更高版本中。这个特性没有在java的低版本虚拟机中实现。
  3. 其他的不同作用域范围的beans(非单例类型)是否存在可见性问题?

我并没有认为Spring是一个不好的框架:它总是令人惊奇,使用它开开发一个项目也让我觉得很快乐。但是我认为开发员应该在多线程开发环境中应该更加小心。

本文提及的问题,一部分应该有开发员自己来负责。如果你在编写线程安全代码,你需要了解你正在做什么。但我也开发Spring框架的人员需要对这些问题来负一些责任:并不是所有的开发任意有很多的编写多线程经验,我猜想很多也不怎么了解Java内存模型。如果这些问题不在Spring的文档中提及,许多开发员会继续保持无知,也会在将来的项目开发中导致问题。

Footnote:
If you want a more in depth explanation of the new Java Memory Model, I suggest ‘Java Concurrency in Practice’ from Brian Goetz, Tim Peierls, Joshua Bloch, Joseph Bowbeer, David Holmes and Doug Lea, or check out the following website The Java Memory Model

感谢我的同事、熟人、以及那些在邮件列表中校对本文的人员。

关键字: Java Spring

 

 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值