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

第十三章 并发编程

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

--James O Coplien

13.1 为什么要并发 

        并发是一种解耦策略。它帮助我们把做什么(目的)和何时(时机)做分解开。在单线
程应用中,目的与时机紧密耦合,很多时候只要查看堆栈追路即可断定应用程序的状态。

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

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

中肯说法

  1. 并发会在性能和编写额外代码上增加一些开销
  2. 正确的并发是复杂的,即便对于简单的问题也是如此;
  3. 并发缺陷并非总能重现,所以常被看做偶发事件而忽略,未被当做真正的缺陷看待;
  4. 并发常常需要对设计策略的根本性修改
13.2 挑战
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 小结

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

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

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

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

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

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值