整洁的并发编程是个复杂话题,这一章我看完收获不是很多,因为直到目前为止都没有怎么涉及过并发编程
13.1 为什么要并发
- 并发是一种解耦策略,可以把 做什么 和 什么时候做 区分开
- 做什么相当于 目的
- 什么时候做相当于 时机
- 解耦目的和时机能明显改进系统的吞吐量和结构
13.1.1 对于并发的误解
- 认为并发总能改进性能
- 并发有时候确实可以改进性能
- 但只有在多个线程或多个处理器之间能分享大量等待时间的前提下管用
- 编写并发程序无需修改设计
- 并发算法的设计有可能与单线程系统的设计相差很大
- 目的与时机的解耦会对系统结构产生巨大影响
- 采用 Web 容器的时候,并不需要了解并发问题,因为容器已经处理的很好
- 容器确实会对并发做一些处理,但没有你想象中的那么强大
- 一般还是需要了解容器在做什么,为什么做,怎么做
13.1.2 对于并发的理解
- 并发会在性能和编写额外代码上增加一些开销
- 正确的并发是复杂的,即便是处理简单的问题,使用并发算法实现也会比使用一般代码更复杂
- 并发的缺陷不一定能马上重现,也不一定总是能复现,所以由于并发导致的问题经常被看做偶发性事件
- 并发基本上都需要对设计策略进行根本性的修改
13.2 挑战
- 将下面这段代码放在两个线程中运行,得到的结果会是三种情况
- Thread1: 43 , Thread2: 44 , lastIdUsed: 44
- Thread1: 44 , Thread2: 43 , lastIdUsed: 44
- Thread1: 43 , Thread2: 43 , lastIdUsed: 43
- 从这三种结果就可以直观的看出多线程导致的并发问题并不能总是复现
public class X {
private int lastIdUsed = 42;
public int getNextId() {
return ++lastIdUsed;
}
}
13.3 并发防御原则
13.3.1 单一权责原则
- 方法 、类 、组件应该只有一个被修改的理由
- 应该将并发代码和其他代码进行分离处理
13.3.2 推论:限制数据作用域
- 在编写并发代码时,要谨记数据封装,对可能被共享访问的数据进行严格限制
13.3.3 推论:使用数据副本
- 尽量避免操作共享数据,尽可能的采用复制对象并且只读的形式去访问数据
- 对于复制的对象,先从多个线程中获取所有复本的结果,然后在单线程中合并最终结果
13.3.4 推论:线程应尽可能地独立
- 每个线程处理一个客户端请求,从单一源头获取获取该客户端的所有请求数据,将需要操作的数据存储为本地变量
13.4 了解 Java 库
- 本小结介绍的是 Java 5 的类库,目前主流使用的 Java 版本已经到 Java8 了
13.5 了解执行模型
- 限定资源
- 并发环境中有着固定尺寸或数量的资源,例如数据库连接数
- 互斥
- 共享数据或资源同时只能被一个线程访问,其他线程只能在当前线程访问结束后才能继续访问
- 线程饥饿
- 一个或多个线程可能在很长时间、甚至永久地处于禁止状态
- 例如总是让执行速度更快的线程先运行,如果资源请求不大,执行速度快的线程总是能先运行先结束,那么执行速度慢的线程就可能一直获取不到数据
- 死锁
- 两个或多个线程互相等待对方执行结束
- 每个线程都拥有其他线程依赖的资源,得不到这个资源线程就无法继续
- 如果此时每个线程都在处理数据,那么就会导致线程之间一直等待,无法终止
- 活锁
- 执行次序一致的线程,每个都想要起步,但其他线程已经处于运行状态
- 如果该线程的起步需要和其他线程保持一致,那么它就会多次尝试起步,却一直无法成功
13.5.1 生产者-消费者模型
- 生产者线程创建某些资源,并放置在缓存或队列中
- 消费者线程从队列中获取这些资源
- 那么这些资源在生产者和消费者之间就是 限定资源
13.5.2 读者-作者模型
- 一个资源主要被读者线程访问,但偶尔作者线程也会访问该资源,这就会导致一定的吞吐量问题
- 平衡读者线程和作者线程的需求,实现正确操作,提供合理的吞吐量、避免线程饥饿
13.5.3 宴席哲学家
- 防止线程竞争资源
13.6 警惕同步方法之间的依赖
- 在一个共享类中只能有一个被
synchronized
标记为同步的方法
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.8 硬编码
- 这是试错代码的一种实现方式,但不是好的方式,因为会影响代码原本的正常逻辑
- 而且很有可能添加之后忘记删除或忘记注释
13.9.9 自动化
- 引入了一个叫 Aspect-Oriented-Framework 的自动化测试工具,没用过
13.10 小结
- 并发代码很难写正确,因为加入多线程和共享数据后,即时是简单代码也会变的复杂
- 编写并打代码第一要诀是 遵循单一权责原则 ,这样至少可以在出现问题的时候将问题范围尽量缩小
- 了解并发问题可能出现的原因
- 操作了共享数据
- 使用了公共资源池
- 没有正确关闭线程
- 没有即时停止循环
- 学习当前环境的类库,了解更多基本算法
- 学习如何找到必须锁定的代码区域并进行锁定,不要随意使用同步关键字