谈谈竞态条件

什么是竞态条件

官方的定义是如果程序运行顺序的改变会影响最终结果,这就是一个竞态条件(race condition).

理解竞态条件首先要知道程序运行不一定是线性的。初学编程的时候都是从“面向过程编程“开始的,一条一条指令打下来,期待着他们会顺序执行。debug的使用也加深了这一认识。不过事实上如果两条紧挨着的指令没有依赖关系,jvm是有可能将他们的运行顺序倒转的。当然这是题外话,最显著的“不按顺序执行“的例子还是多线程程序。

Runnable r1 = () -> { // do something };
Runnable r2 = () -> { // do another thing };
new Thread(r1).start();
new Thread(r2).start();

上述程序定义了两个Runnable实例,并用它们新建两个线程,虽然r1先于r2开始,但他们内部的代码谁先谁后就不由而知了。

如果一段程序运行多次的结果不一致(排除生成随机数的情况),那这就可能是竞态条件的体现。比如最典型的例子,两个线程同时把一个类的静态成员做50词自增加1的操作,即

SomeClass.someMember++;

写在两个线程中,都运行50次,运行结束以后用主线程去取这个变量的值几乎不可能是100. 有的时候是97,有的时候是98,这是用来说明竞态条件的最有效例子。

自增加操作其实是三个操作的组合:
1. 取该变量的值
2. 给这个取到的值+1
3. 把计算好的值赋给该变量

学过计算机系统的同学会知道这三个操作的区别,取值是从内存取到寄存器,+1以后值还是在寄存器,只有在赋值完成后,内存中的该变量的值才会变化。我们的竞态条件发生的原因就是在一个+1的值还没有赋给变量的时候,另一个线程开始读取内存中的该变量的值,等这个线程完成+1、赋值以后,他的工作其实和之前那个线程是一样的,有若干类似现象出线以后,就会导致最后的值永远达不到100。

工作中遇到的问题

应用一次崩溃后,日志中查到的问题是cursor还在使用,但数据库environment已经关闭,程序throw a DatabaseException, 然而这个Exception没有被任何try-catch语句抓住,所以导致全局陷落。

这个问题有两个维度,第一是为什么会产生异常,第二就是没有正确处理异常,一个可靠的系统不应该因为一些误操作而崩溃。我们这里只讨论第一个维度。

先说一下我使用的Berkeley DB的一些前提条件,任何操作必须在environment打开的情况下执行;官方文档建议操作结束以后关闭environment。我的应用是一个web application, 最初的方法就是将environment一直开着,要关闭的时候我特意写了一个ShutdownServlet, 用来关闭environment并退出系统。然而这种操作对于需要经常更新war包的系统很不方便,由于environment没有关闭,它的锁一直存在,如果强行更换war包会导致重启后数据库打不开。

我希望war包替换掉系统就能自动更新,所以后来设计了每次数据库操作都关闭environment的结构。这确实解决了war包更新的问题,但也引入了新的“deadlock” 问题。由于网络应用的多线程本质,系统会出线两个request都需要读写数据库的情况,由于他们都是要在操作结束后关闭environment的,一旦操作有先后,就会发生一个线程还在工作,另一个线程就把数据库关掉了的情况。

脱困方法

为了解决这个问题,我在数据库操作的每个方法开头都加入了判断environment是否打开的语句,如果关闭了就重新打开。这在一定程序上缓解了死锁,但也没有完全解决问题,同样的崩溃还是会发生。这其实就是一个竞态条件的例子,

public void insert(Object object) {
    if (!myEnv.isValid()) {
        init();
    }
    // go on with insert logic
    doOneThing();
    doAnotherThing();
    shutdown();
}

假象一下这样的情况,一个线程运行到doOneThing() 的时候,另一个线程开始进入这个方法,判断environment是否打开if (!myEnv.isValid()), 判断为“是“,继续操作,这时候之前的线程执行完毕,关闭了数据库,那么另一个以为数据库打开的线程就会出线问题,继续操作的时候发现environment已经关闭,这就会抛出异常。

解决方法是把每个public 方法加上synchronized 关键字,让每个数据库操作一次只能有一个线程来访问,就可以避免上述的死锁和竞态条件。

总结

这篇文章首先介绍了竞态条件的定义,说明要认识竞态条件,原有的程序顺序执行的概念需要被颠覆,然后通过两个线程同时自增加一个变量的例子初步介绍竞态条件出线的内存机理。

我通过工作中出线的数据库死锁问题,描述了我亲身经历的竞态条件事故 —— 程序崩溃,和我一步步的解决方法:首先方法体内加判断语句,发现不完全解决问题以后决定给方法加锁,希望能给读者提供一个参考,毕竟多线程是比较advanced的内容。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值