并发
13.1 为什么要并发
并发并非总能改进性能,只在多个线程或处理器之前分享大量等待时间时管用
并发软件的中肯说法
并发会在性能和编写额外代码上增加一些开销
正确的并发是复杂的,即便对于简单的问题也是如此
并发缺陷并非总能重现,所以常被看做偶发事件而忽略,未被当做真的缺陷看待
并发常常需要对设计策略的根本性修改
13.2 挑战
13.3 并发防御原则
13.3.1单一权责原则(SRP)
井发相关代码有自己的开发、修改和调优生命周期
开发相关代码有自己要对付的挑战,和非井发相关代码不同,而且往往更为困难
即便没有周边应用程序增加的负担,写得不好的并发代码可能的出错方式数量也己经足具挑战性
建议: 分离并发代码与其他代码
13.3.2 推论:限制数据作用域
- 解决方案之一是采用synchronized关键字在代码中保护一块使用共享对象的临界区(critical 革section)。限制临界区的数量很重要。更新共享数据的地方越多就越可能
会忘记保护一个或多个临界区一一破坏了修改共享数据的代码
得多花力气保证一切都受到有效防护(破坏了DRY原则)
DRY原则 - 不要重复自己
- 很难找到错误源,也很难判断错误源
13.3.3 推论:使用数据副本
13.3.4 推论:线程应该尽可能独立
让每个钱程在自己的世界中存在,不与其他线程共享数据。每个线程处理一个客户端请求,从不共享的源头接纳所有请求数据,存储为本地变量。
建议: 尝试将数据分解到可被独立线程(可能在不同处理器上)操作的独立子集。
13.4 了解java库
使用类库提供的线程安全群集:
使用executor框架(executor framework)执行无关任务1
尽可能使用非锁定解决方案;
有几个类并不是统程安全的。
表13-1 | 支持高级并发类 |
---|---|
ReentrantLock | 可在一个方法阳获取、在另一方法中释放的锁 |
Semaphore | 经典的 “信号” 的一种实现,有计数器的锁 |
CountDownLatch | 在释放所有等待的线程之前,等待指定数量事件发生的锁。这样,所有线程都平等 地儿乎同时启动 |
遗留: ReentrantLock、Semaphore
建议: 检读可用的类。对于Java,掌握java.util.concurrent,ava.util.oncurrent.atomic和java.util.concurrent.locks
13.5 了解执行模型
表13-2 | 基础定义 |
---|---|
限定资源 | 并发环境中有着固定尺寸或数量的资源。例如数据库连接和固定尺寸读/写缓存等 |
互斥 | 每一时刻仅有一个线程能访问共享数据或共享资源 |
线程饥饿 | 一个或一组线程在很长时间内或永久被禁止。例如,总是让执行得快的线程先运行, 假如执行得快的线程没完没了,川执行时间长的线程就会 “ 挨饿” |
死锁 | 两个或多个线程互相等待执行结束。每个线程都拥有其他线程需要的资源,得不到 其他线程拥奇的资源,就无法终止 |
活锁 | 执行次序一致的线程,每个都想要起步,但发现其他线程己经 “在路上” 。由于竟步的原因,线程持续尝试起步,但在很长时间内却无法如愿,甚至永远无法启动 |
遗留活锁
13.5.1生产者消费者模型
mqtt
一个或多个生产者线程创建某些工作,并置于缓存或队列中。一个或多个消费者线程从队列中获取并完成这些工作。生产者和消费者之间的队列是一种限定资源。
13.5.2 读者-作者模型
13.5.3 宴席哲学家
死锁
活锁
吞吐量
效率降低
遗留: 吞吐量
13.6 警惕同步方法之间的依赖
建议: 避免使用一个共享对象的多个方法
有时必须使用一个共享对象的多个方法。在这种情况发生时,有3种写对代码的手段
基于客户端的锁定一一客户端代码在调用第一个方法前锁定服务端,确保锁的范围
基于服务端的锁定一一在服务端内创建锁定服务端的方法, 调用所有方法, 然后解锁。让客户端代码调用新方法:
适配服务端一一创建执行锁定的中间层。这是一种基于服务端的锁定的例子, 但不修改原始服务端代码。
13.7 保持同步区域微小
建议:尽可能减小同步区域。
13.8 很难正确的关闭代码
建议尽早考虑关闭问题, 尽早令其工作正常。这会花费比你预期更多的时间。检视既有算法,因为这可能会比想象中难得多
13.9 测试线程代码
将伪失败看作可能的线程问题
先使非线程代码可工作
编写可插拔的钱程代码
编写可调整的线程代码
运行多于处理器数量的线程
在不同平台上运行
调整代码并强迫错误发生
13.9.1 将伪失败看作可能的线程问题
建议:不要将系统错误归咎于偶发事件。
13.9.2 先使非线程代码可工作
建议:不要同时追踪非线程缺陷和线程缺陷。确保代码在线程之外可工作。
- POJO 普通java对象指未完全遵从特定的Java对象模型、约定或框架(如EJB)的Java对象
13.9.3 编写可插拔的线程代码
单线程与多个线程在执行时不同的情况
线程代码与实物或测试替身互动
用运行快速、缓慢和 有变动的测试替身执行
将测试配置为能运行一定数量的选代
建议: 编写可插拔的线程代码,这样就能在不同的配置环境下运行
13.9.4 编写可调整的线程代白
13.9.5 运行多于处理器数量的线程
13.9.6 在不同平台上运行
13.9.7 装置试错代码
怎么才能增加捕捉住如此罕见之物的机会?可以装置代码,增加对Object.wait()、 Object.sleep()、Object.yield()和Object.priority()等方法的调用,改变代码执行顺序。
yiled():让步
wait():等待
sleep():休眠
yiled是让步,会使当前线程由运行状态进入到就绪状态,让其他优先级高线程先执行,但是如果是同一优先级的线程,那么谁先执行就不确定了.它不会释放锁
wailt等待,会使当前线程进入阻塞状态,并且会释放锁
sleep休眠,会使当前线程进入休眠阻塞状态,但不会释放锁
13.10 小结
并发代码很难写正确。加入多线程和共享数据后,简单的代码也会变成噩梦。要编写井发代码, 就得严格地编写整洁的代码, 否则将面临微细和不频繁发生的失败。
要诀是遵循单 权责原则。将系统切分为分离了线程相关代码和线程无关代码的POJO。确保在测试线程相关代码时只是在测试,没有做其他事情。线程相关代码应该保持短小和集中。
了解并发问题的可能原因:对共享数据的多线程操作,或使用了公共资源地。类似平静关闭或停止循环之类边界情况尤其棘手。
学习类库, 了解基本算法。理解类库提供的与基础算法类似的解决问题的特性。
学习如何找到必须锁定的代码区域并锁定之。不要锁定不必锁定的代码。避免从锁定区域中调用其他锁定区域。这需要深刻理解某物是否己共享。尽可能减少共享对象和共享范围。修改对象的设计,向客户代码提供共享数据,而不是迫使客户代码管理共享状态。
问题会跳出来。那种在早期没跳出来的问题往往是偶发的。这种所谓偶发问题, 通常仅在高负载下出现或者偶然出现。所以,你要能在不同平台上、以不同配置持续重复运行线程代码。跟随TDD三要则而来的可测试性意味着某种程度的可插拔性, 从而提供了在大量在不同配置下运行代码的必要支持。
如果花点时间装置代码,就能极大地提升发现错误代码的机会。可以手工做, 也可以使用某种自动化技术。在将线程代码投入生产环境前,就要尽可能多地运行它。
只要采用了CleanCode的做法, 做对的可能性就有翻天覆地的提高。