争用条件
据说 争用条件 存在于这样的系统中多个线程之间存在对共享资源的竞争而胜出者决定系统的行为Allen Holub 在他撰写的文章 programming Java threads in the real world 提供了一个带有这样 bug 的简单的多线程程序示例在沖突的访问请求之间进行不正确同步的另一个更可怕的后果是 数据崩溃此时共享的数据结构有一部分由一个线程更新而另一部分由另一个线程更新在这种情况下系统的行为不是按照胜出线程的意图进行系统根本不按照任何一个线程的意图行动所以两个线程最后都将以失败告终
死锁
死锁 的情况是指线程由于等候某种条件变成真(例如资源可以使用)但是它等候的条件无法变成真因为能够让条件变成真的线程在等候第一个线程做某件事这样两个线程都在等候对方先采取第一步所以都无法做事
活动锁
活动锁 与 死锁 不同它是在线程实际工作的时候发生的但这时还没有完成工作这通常是在两个线程交叉工作的时候发生所以第一个线程做的工作被另一个线程取消一个简单的示例就是每个线程已经拥有了一个对象同时需要另外一个线程拥有的另外一个对象可以想像这样的情况每个线程放下自己拥有的对象捡起另外一个线程放下的对象显然这两个线程会永远都运行在上锁这一步操作上结果是什么都做不成(常见的真实示例就是两个人在狭窄的走廊相遇每个人都礼貌地让到另一边让对方先行但却在相同的时间都让到同一边了所以两个人还都没法通过这种情况会持续一些时间然后两个人都从这边闪到那边结果还是一点进展也没有)
资源耗尽
资源耗尽又称为 线程耗尽是 Java 语言的 wait/notify 原语无法保证 liveness 的后果Java 强制这些方法要拥有它们等候或通知的对象的锁在某个线程上调用的 wait() 方法在开始等候之前必须释放监视器锁然后在从方法返回并获得通知之后必须再次重新获得锁因此Java 语言规范在锁本身之外还描述了一套与每个对象相关的 等候集(wait set)一旦线程释放了对象上的锁(在 wait 的调用之后)线程就会放在这个等候集上
多数 JVM 实现把等候线程放在队列中所以如果在通知发生的时候还有其他线程在等候监视器那么就会把一个新线程放在队列尾部而它并不是下一个获得锁的线程所以等到被通知线程实际得到监视器的时候通知该线程的条件可能已经不再为真所以它不得不再次 wait这种情况可能无限持续下去从而造成运算工作上浪费(因为要反复把该线程放入等候集和从中取出)和线程耗尽
贪心哲学家的寓言
演示这种行为的原型示例是 Peter Welch 教授描述的聪明人没有鸡肉在这个场景中考虑的系统是一所由五位哲学家一位厨师和一个食堂组成的学院所有的哲学家(除了一位)都要想想(在代码示例中考虑的时间是 秒)之后才去食堂取饭而贪心的哲学家则不想把时间浪费在思考上 —— 相反他一次又一次地回到食堂企图拿到鸡肉来吃
厨师按照一批四份的定量准备鸡肉每准备好一批就送到食堂贪心的哲学家不断地去厨房但他总是错过食物!事情是这样的他第一次到的时候时间太早厨师还没开火因此贪心的哲学家只好干等着(通过 wait() 方法调用)在开饭的时候(通过 notify() 方法调用)贪心的哲学家再一次回到食堂排队等候但是这次在他前来等候的时候他的四位同事已经到了所以他在食堂队列中的位置在他们后面他的同事们把厨房送来的一批四份鸡肉全部拿走了所以贪心的哲学家又要在一边等着了 可怜(也可能是公平的) 他永远处在这个循环之外
验证的问题
一般来说很难按照普通的规范对 Java 编程的多线程程序进行验证同样开发自动化工具对于常见的并发问题(例如死锁活动锁和资源耗尽)进行完整而简单的分析也不太容易——特别是在任意 Java 程序中或者在缺乏并发的正式模型的时候
更糟的是并发性问题出了名的变化多端难于跟蹤每个 Java 开发人员都曾经听说过(或者亲自编写过)这样的 Java 程序经过严格分析而且正常运行了相当一段时间没有表现出潜在的死锁然后突然有一天问题发生了结果弄得开发团队经历许多的不眠之夜来试图发现并修补根本原因
一方面多线程 Java 程序容易发生的错误非常不明显有可能在任意什么时候发生另一方面完全有可能这些 bug 在程序中从不出现问题取决于一些不可知的因素多线程程序的复杂本质使得人们很难有效地对其进行验证没有一套现成的规则可以找出多线程代码中的这类问题也无法确切地证明这些问题不存在这些导致许多 Java 开发人员完全避开多线程应用程序的设计和开发即使用并发和并行的方式对系统进行建模会非常棒他们也不使用多线程
确实想进行多线程编程的开发人员通常准备好了以下一个或两个解决方案(至少是一部分)
长时间艰苦地测试代码找出所有出现的并发性问题诚心地希望到应用程序真正运行地时候已经发现并修复了所有这类问题
大量运行设计模式和为多线程编程建立的指导原则但是这类指导原则只在整个系统都按照它们的规范设计的时候才有效没有设计规则能够覆盖所有类型的系统
虽然知道的人不多但是对于编写(然后验证)正确的多线程应用程序这一问题还有第三个选项使用称为通信顺序进程( Communicating Sequential ProcessesCSP)的精确的线程同步的数学理论可以在设计时最好地处理死锁和活动锁之类的问题CSP 由 CAR Hoare 与 世纪 年代后期设计CSP 提供了有效的方法证明用它的构造和工具构建的系统可以免除并发的常见问题
结束语
在这份面向 Java 程序员的 CSP 全面介绍中我把重点放在克服多线程应用程序开发常见问题的第一步上即了解这些问题我介绍了 Java 平台上目前支持的多线程编程构造解释了它们的起源讨论了这类程序可能会有的问题我还解释了用正式理论在任意的大型的和复杂的应用程序中清除这些问题(即竞争冒险死锁活动锁和资源耗尽)或者证明这些问题不存在的困难
[] []