《代码整洁之道》第十三章

11 Apr 2022

Reading time ~12 minutes

13.1 为什么要并发

并发是一种解耦策略。它帮助我们把做什么(目的)和何时做(时机)分解开。

解耦目的与时机能明显地改进应用程序的吞吐量和结构。

迷思与误解

常见的迷思与误解:

  • 并发总能改进性能
  • 编写并发程序无须修改设计
  • 在采用Web或EJB容器的时候,理解并发问题并不重要。

一些有关编写并发软件的中肯说法:

  • 并发会在性能和编写额外代码上增加一些开销
  • 正确的并发是复杂的
  • 并发缺陷并非总能复现
  • 并发常常需要对设计策略做根本性修改

13.2 挑战

需要理解Just-In-Time编译器如何对待生成的字节码,还要理解Java内存模型认为什么东西具有原子性。

13.3 并发防御原则

13.3.1 单一权责原则

单一权责原则认为,方法/类/组件应当只有一个修改的理由。

  • 并发相关代码有自己的开发、修改和调优生命周期
  • 开发相关代码有自己要应对的挑战,它和非并发相关代码不同,而且往往更为困难
  • 即便没有增加周边应用程序的负担,写得不好的并发代码可能的出错方式数量也已经足具挑战性

分离并发相关代码与其他代码。

13.3.2 推论:限制数据作用域

关键字synchronized在代码中保护一块使用共享对象的临界区。

限制临界区的数量很重要。更新共享数据的地方越多,就越可能:

  • 会忘记保护一个或多个临界区——破坏了修改共享数据的代码
  • 得多花力气保证一切都受到有效保护
  • 很难找到错误源,也很难判断错误源

谨记数据封装;严格限制对可能被共享的数据的访问。

13.3.3 推论:使用数据副本

避免共享数据的好方法之一是,从开始就避免共享数据。

13.3.4 推论:线程应尽可能地独立

让每个线程在自己的世界中存在,而不与其他线程共享数据。

尝试将数据分解为可被独立线程操作的独立子集。

13.4 了解Java库

  • 使用类库提供的线程安全群集
  • 使用executor框架执行无关任务
  • 尽可能使用非锁定解决方案
  • 有几个类并不是线程安全的

13.5 了解执行模型

限定资源:并发环境中有着固定尺寸或数量的资源。

互斥:每个时刻仅有一个线程能访问共享数据或共享资源

线程饥饿:一个或一组线程在很长时间内或永久被禁止。

死锁:两个或多个线程互相等待执行结束。每个线程都拥有其他线程需要的资源,如果得不到其他线程拥有的资源,就无法终止。

活锁:执行次序一致的线程,每个都想要起步,但发现其他线程已经”在路上”。由于竞步的原因,线程会持续尝试起步,但在很长时间内却无法如愿,甚至永远无法启动。

13.5.1 生产者-消费者模型

一个或多个生产者线程创建某些工作,并置于缓存或队列中。一个或多个消费者线程从队列中获取并完成这些工作。生产者和消费者之间的队列是一种限定资源。

13.5.2 读者-作者模型

当存在一个主要为读者提供信息源,但只偶尔被作者线程更新的共享资源,吞吐量就会是个问题。

13.5.3 宴席哲学家

如果没有用心设计,就会出现竞争式系统,就会遭遇死锁、活锁、吞吐量和效率降低等问题。

13.6 警惕同步方法之间的依赖

如果在同一个共享类中有多个同步方法,系统就可能写得不太正确了。

避免使用一个共享对象的多个方法。

有时必须使用一个共享对象的多个方法时:

  • 基于客户端的锁定
  • 基于服务端的锁定
  • 适配服务端

13.7 保持同步区域微小

应该尽可能少地设计临界区。

尽可能减少同步区域。

13.8 很难编写正确的关闭代码

平静关闭很难做到。

尽早考虑关闭问题,今早令其正常工作。

13.9 测试线程代码

13.9.1 将伪失败看作可能的线程问题

不要将系统错误归咎于偶发事件

13.9.2 先使非线程代码可工作

不要同时追踪非线程缺陷和线程缺陷,确保代码在线程之外可工作。

13.9.3 编写可插拔的线程代码

编写可插拔的线程代码,这样就能在不同的配置环境下运行。

13.9.4 编写可调整的线程代码

要获得良好的线程平衡,常常需要反复试验。

13.9.5 运行多于处理器数量的线程

13.9.6 在不同的平台上运行

尽早并经常地在所有目标平台上运行线程代码。

13.9.7 装置试错代码

13.9.7.1 硬编码

手工向代码中插入wait()、sleep()、yield()和priority()的调用。

13.9.8.2 自动化

可以使用Aspect-Oriented Framework、CGLIB或ASM之类的工具,通过编程来装置代码。



Reading NotesClean Code Share Tweet +1