第13章 并发编程
“对象是过程的抽象。线程是调度的抽象。”
编写整洁的并发程序很难——非常难。
并发是一种解耦策略。它帮助我们把做什么和何时做分解开,即分离了目的和时机。
解耦目的与时机能明显改进应用程序的吞吐量和结构。
从结构的角度来看,应用程序看起来更像是许多太协同工作的额计算机,而不是一个大循环。
有些系统对相应时间和吞吐量有要求,需要手工编写并发解决方案。
并发有时能改进性能,但只在多个线程或处理器之间能分享大量等待时间的时候管用。
并发算法的设计有可能与单线程系统的设计极不相同。目的与时机的解耦往往对系统结构产生巨大影响。
你最好了解容器在做什么,了解如何对付并发更新、死锁等问题。
正确的并发是复杂的,即便对于简单的问题也是如此。
并发缺陷并非总能重现,常被看做偶发事件而忽略。
并发常常需要对设计策略的根本性修改。
建议:分离并发相关代码与其他代码。
建议:谨记数据封装;严格限制对可能被共享的数据的访问。
使用数据副本,避免共享数据。
建议:尝试将数据分解到可被独立线程操作的独立子集。
建议:检读可用的类库。
了解执行模型:
-
生产者-消费者模型
生产者和消费者之间的队列是一种限定资源。
-
读者-作者模型
存在一个主要为读者线程提供信息源,但只偶尔被作者线程更新的共享资源,吞吐量就会是个问题。 更新会影响吞吐量。 作者线程倾向于长期锁定许多读者线程,从而导致吞吐量问题。 挑战之处在于平衡读者线程和作者线程的需求,实现正确操作,提供合理的吞吐量,避免线程饥饿。
-
宴席哲学家
进程竞争资源的情形。 需要用心设计,避免死锁、活锁、吞吐量和效率低下等问题。
建议:学习这些基础算法,理解其解决方案。
同步方法之间的依赖会导致并发代码中的狡猾缺陷。
建议:避免使用一个共享对象的多个方法。
有时必须使用一个共享对象的多个方法,此时有3种写代码的手段:
-
基于客户端的锁定
客户端代码在调用第一个方法锁定服务端,确保锁的范围覆盖了调用最后一个方法的代码
-
基于服务端的锁定
在服务端内创建锁定服务端的方法,调用所有方法,然后解锁。让客户端调用新方法。
-
适配服务端
创建执行锁定的中间层。 这是一种基于服务端锁定的例子,但不修改原始服务端代码。
锁是昂贵的,因为它们带来了延迟和额外开销。
临界区应该被保护起来。所以,应该尽可能少地设计临界区。
建议:尽可能减小同步区域。
编写永远运行的系统,与编写运行一段时间后平静地关闭的系统是两码事。
平静关闭很难做到。
常见问题与死锁有关,线程一直等待永远不会到来的信号。
证明代码的正确性不切实际。
测试并不能保证正确性。
然而,好的测试却能尽量降低风险。
建议:编写有潜力暴露问题的测试,在不同的编程配置、系统配置和负载条件下频繁运行。
运行多于处理器数量的线程;
在不同平台上运行;
调整代码并强迫错误发生;
多数开发者缺乏有关线程如何与其他代码互动的直觉。
建议:不要将系统错误归咎于偶发事件。
先确保线程之外的代码可工作。
建议:不要同时追踪非线程缺陷和线程缺陷。先确保代码在线程之外可工作。
建议:编写可插拔的线程代码,这样就能在不同的配置环境下运行。
任务交换越频繁,越有可能找到错过临界区或导致死锁的代码。
不同操作系统有着不同线程策略,这影响了代码的执行。
建议:尽早并经常地在所有目标平台上运行线程代码。
有问题的代码,最好尽早、尽可能多地通不过测试。
第一要诀是遵循单一权责原则。将系统切分为分离了线程相关代码和线程无关代码的POJO。
了解并发问题的可能原因:对共享数据的多线程操作,或使用了公共资源池。
学习类库,了解基本算法。理解类库提供的与基础算法类似的解决问题的特性。
学习如何扎到必须锁定的代码区域并锁定之。不要锁定不必要的代码。
只要采用了整洁的做法,做对的可能性就有翻天覆地的提高。