clean code-代码整洁之道 阅读笔记(第十三章)

第十三章 并发编程

"对象是过程的抽象。线程是调度的抽象。"

--James O Coplien

13.1 为什么要并发

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

程应用中,目的与时机紧密耦合,很多时候只要查看堆栈追路即可断定应用程序的状态。

解耦目的与时机能明显地改进应用程序的吞吐量和结构。从结构的角度来看,应用程序看起来更像是许多台协同工作的计算机,而不是一个大循环。系统因此会更易于被理解,给出了许多切分关注面的有力手段。

迷思与误解
  1. 并发总能改进性能
  2. 编写并发程序无需修改设计
  3. 在采用Web或EJB容器的时候,理解并发问题并不重要

中肯说法

  1. 并发会在性能和编写额外代码上增加一些开销
  2. 正确的并发是复杂的,即便对于简单的问题也是如此;
  3. 并发缺陷并非总能重现,所以常被看做偶发事件而忽略,未被当做真正的缺陷看待;
  4. 并发常常需要对设计策略的根本性修改
13.2 挑战
java 复制代码
public class X{
    private int lastIdUsed;
    public int getNextId(){
        return ++lastIdUsed;
    }
}

比如,创建x的一个实体,将lastIdUsed设置为42,在两个线程中共享这个实体。假设这两个线程都调用getNextId()方法,结果可能有三种输出:

  • 线程一得到值43,线程二得到值44,lastIdUsed为44;
  • 线程一得到值44,线程二得到值43,lastIdUsed为44;
  • 线程一得到值43,线程二得到值43,lastIdUsed为43。

就生成的字节码而言,对于在getNextId方法中执行的那两个线程,有12870种不同的可能执行路径。如果lastIdUsed的类型从int变为long,则可能路径的数量将增至2704156种。当然,多数路径都得到正确结果。问题是其中一些不能得到正确结果。

13.3 并发防御原则
13.3.1 单一权责原则

问题:

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

**建议:**分离并发相关代码与其他代码。

13.3.2 推论:限制数据作用域

两个线程修改共享对象的同一字段时,可能互相干扰,导致未预期的行为。解决方案之一是采用synchronized关键字在代码中保护一块使用共享对象的临界区(criticalsection)。

可能出现的问题:

  1. 你会忘记保护一个或多个临界区------破坏了修改共享数据的代码码;
  2. 得多花力气保证一切都受到有效防护(破坏了DRY原则);
  3. 很难找到错误源,也很难判断错误源。

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

13.3.3 推论:使用数据复本

避免共享数据的好方法之一就是一开始就避免共享数据 。在某些情形下,有可能复制对象并以只读方式 对待。在另外的情况下,有可能复制对象,从多个个线程收集所有复本的结果,并在单个线程中合并这些结果。

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

让每个线程在自己的世界中存在,不与其他线程共享数据。每个线程处理一个客户端请求,从不共享的源头接纳所有请求数据,存储为本地变量。这样一来,每个线程都像是世界中的唯一线程,没有同步需要。

**建议:**尝试将数据分解到可被独立线程(可能在不同处理器上)操作的独立子集。

13.4 了解Java库
  • 使用类库提供的线程安全群集;
  • 使用executor框架(executorframework)执行无关任务;
  • 尽可能使用非锁定解决方案;
  • 有几个类并不是线程安全的。
13.5 了解执行模型
13.5.1 生产者-消费者模型

生产者和消费者之间的队列是一种限定资源。

13.5.2 读者-作者模型

当存在一个主要为读者线程提供信息源,但只偶尔被作者线程更更新的共享资源,吞吐量就会是个问题。增加吞吐量,会导致线程饥饿和过时信息的累积。更新会影响吞吐量。

挑战之处在于平衡读者线程和作者线程的需求,实现正确操作,提供合理的吞吐量,避免线程饥饿。

13.5.3 宴席哲学家

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

可能遇到的并发问题,大多数都是这三个问题的变种。

**建议:**学习这些基础算法,理解其解决方案。

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

**建议:**避免使用一个共享对象的多个方法。

必须使用一个共享对象的多个方法的3种手段:

  1. 基于客户端的锁定------客户端代码在调用第一个方法前锁定服务端,确保锁的范围覆盖了调用最后一个方法的代码;
  2. 基于服务端的锁定------在服务端内创建锁定服务端的方法,调用所有方法,然后解锁。让客户端代码调用新方法;
  3. 适配服务端------创建执行锁定的中间层。这是一种基于服务端的的锁定的例子,但不修改原始服务端代码。
13.7 保持同步区域微小

关键字synchronized制造了锁。锁是昂贵的,因为它们带来了延迟和额外开销。

另一方面,临界区应该被保护起来。所以,应该尽可能少地设计临界区。

将同步延展到最小临界区范围之外,会增加资源争用、降低执行效率。

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

平静关闭很难做到。常见问题与死锁有关,线程一直等待永远不会到来的信号。

**建议:**尽早考虑关闭问题,尽早令其工作正常。这会花费比你预期更多的时间。检视既有算法,因为这可能会比想象中难得多。

13.9 测试线程代码

**建议:**编写有潜力曝露问题的测试,在不同的编程配置、系统配置和负载条件下频繁运行。如果测试失败,跟踪错误。别因为后来测试通过了后来的运行就忽略失败。

  • 将伪失败看作可能的线程问题 => 不要将系统错误归咎于偶发事件
  • 先使非线程代码可工作 => 不要同时追踪非线程缺陷和线程缺陷。
  • 编写可插拔的线程代码
  • 编写可调整的线程代码
  • 运行多于处理器数量的线程
  • 在不同平台上运行
  • 调整代码并强迫错误发生。
13.10 小结

第一要诀是遵循单一权责原则。

了解并发问题的可能原因。

学习类库,了解基本算法。

学习如何找到必须锁定的代码区域并锁定之。不要锁定不必针锁定的代码。

要能在不同平台上、以不同配置持续重复运行线程代码。

如果花点时间装置代码,就能极大地提升发现错误代码的机会。

相关推荐
吉大一菜鸡33 分钟前
FPGA学习(基于小梅哥Xilinx FPGA)学习笔记
笔记·学习·fpga开发
CCSBRIDGE3 小时前
Magento2项目部署笔记
笔记
亦枫Leonlew4 小时前
微积分复习笔记 Calculus Volume 2 - 5.1 Sequences
笔记·数学·微积分
爱码小白4 小时前
网络编程(王铭东老师)笔记
服务器·网络·笔记
LuH11245 小时前
【论文阅读笔记】Learning to sample
论文阅读·笔记·图形渲染·点云
一棵开花的树,枝芽无限靠近你6 小时前
【PPTist】组件结构设计、主题切换
前端·笔记·学习·编辑器
犬余7 小时前
设计模式之桥接模式:抽象与实现之间的分离艺术
笔记·学习·设计模式·桥接模式
数据爬坡ing8 小时前
小白考研历程:跌跌撞撞,起起伏伏,五个月备战历程!!!
大数据·笔记·考研·数据分析
咖肥猫8 小时前
【ue5学习笔记2】在场景放入一个物体的蓝图输入事件无效?
笔记·学习·ue5
郭尘帅6669 小时前
Ajax学习笔记
笔记·学习·ajax