阅读 166

《代码整洁之道》阅读笔记 13并发(编写整洁的并发代码建议)

对象是过程的抽象,线程时调度的抽象。”——James O Coplien 在这里插入图片描述

1.为什么要并发

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

2.相对于单线程的目的和时机的紧密耦合,解耦目的与时机能明显地改进应用程序的吞吐量和结构

3.单线程程序许多时间花在等待web套接字I/O结束上面,通过采用同时访问多个站点的多线程算法,就能改进性能

4.同时也存在一些副作用

  • 并发总能改进性能:只在多个线程或处理器之间能分享大量等待时间的时候管用
  • 编写并发程序无需修改设计:可能与单线程系统的设计极不相同。目的与时机的解耦往往对系统结构产生巨大影响
  • 在采用Web或EJB容器时,理解并发问题并不重要(实际上,最好了解容器在做什么)

5.有关编写并发软件的中肯的说法:

  • 并发会在性能和编写额外代码上 增加一些开销
  • 正确的并发是复杂的,即使对于简单的问题也是如此
  • 并发缺陷并非总能重现,所以常被看做偶发事件而忽略,未被当做真的缺陷看待
  • 并发常常需要 对设计策略的根本性修改

2.挑战

1.线程在执行代码时有许多可能路径可行,有些路径会产生错误的结果

public class X {
  private int lastIdUsed = 42;

  public int getNextId() {
    return ++lastIdUsed;
  }
}
复制代码

将这段代码放在两个线程中运行,得到的结果会是三种情况

Thread1: 43 , Thread2: 44 , lastIdUsed: 44
Thread1: 44 , Thread2: 43 , lastIdUsed: 44
Thread1: 43 , Thread2: 43 , lastIdUsed: 43
复制代码

从这三种结果就可以直观的看出多线程导致的并发问题并不能总是复现

当两个线程相互影响时就会出现不可预期的情况。这是因为线程在执行那行java代码时有许多可能路径可行,有些路径会产生错误的结果。回答这个问题需要理解 Just-IN-Time编译器如何对待生成的字节码,还要理解java内存模型认为什么东西具有原子性。(答案在深入理解Java虚拟机中

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

3.并发防御原则

(1)单一权责原则SRP:分离并发相关代码和其他代码

(2)限制数据作用域(谨记数据封装;严格限制对可能被共享的数据的访问)

(3)使用数据复本

(4)线程应尽可能地独立(尝试将数据分解到可被独立线程(可能在不同处理器上)操作的独立子集) (每个线程处理一个客户端请求,从不共享的源头接纳所有请求数据,存储为本地变量)

4.了解Java库

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

检读可用的类。对于Java,掌握Java.util.concurrent, java.util.concurrent.atomic java.util.concurrent.locks

5.了解执行模型

1.一些基础定义

限定资源并发环境中有着固定尺寸或数量资源。例如数据连接
互斥每一时刻仅有一个线程能访问共享数据或资源
线程饥饿一个或一组线程在长时间内或永久禁止,列入执行得快的线程一直运行,导致执行慢的永远得不到执行机会
死锁两个或多个线程互斥等待执行结束,而相互之间持有对方结束的资源
活锁执行次序一致的线程,每个都想起步,但发现其他线程已经在路上了,由于竞步的原因,线程持续尝试起步,但长时间都不发如愿
2.多线程模型大致有一下三种,而且大部分问题都是三种模式中存在的问题的变种
1.生产者消费者模型----限定资源

2.读者-作者模型------线程饥饿、吞吐量降低

3.竞争式系统----死锁、活锁、吞吐量和效率

6.警惕同步方法之间的依赖

应避免使用一个共享对象的多个方法,如果必须使用,有3种可以应对的手段 1.基于客户端的锁定-客户端代码在调用第一个方法前锁定服务端,确保锁的范围覆盖了调用的最后一个方法的代码

2.基于服务端锁定-在服务端内创建锁定客服端的方法,调用所有方法,然后解锁

3.适配服务端-创建执行锁定的中间层

7.保持同步区域微小

关键字synchronized制造了锁。锁是昂贵的。

同一个锁维护的所有代码区域在任一时刻保证只有一个线程执行,因为它们带来了延迟和额外开销,==临界区应该被保护起来,应该尽可能少地设计临界区==

8.很维编写正确的关闭代码

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

2.建议:尽早考虑关闭问题,尽早令其工作正常

9.测试线程代码

测试不能保证正确性,然而,好的测试却能尽量降低风险。 这对于所有单线程解决方案都是对的。当有两个或多个线程使用同一代码段和共享数据,事情就变得复杂了

  • 编写有潜力曝露问题的测试,在不同的编程配置、系统配置和负载条件下频繁运行。如果测试失败,跟踪错误。别因为后来测试通过了后来的运行就忽略失败
  • 将伪失败看作可能的线程问题:线程代码导致“不可能失败的”失败,不要将系统错误归咎于偶发事件
  • 先使非线程代码可工作:不要同时追踪非线程缺陷和线程缺陷,确保代码在线程之外可工作
  • 编写可插拔的线程代码,能在不同的配置环境下运行
  • 编写可调整的线程代码:允许线程依据吞吐量和系统使用率自我调整
  • 运行多于处理器数量的线程:任务交换越频繁,越有可能找到错过临界区域导致死锁的代码
  • 在不同平台上运行:尽早并经常地在所有目标平台上运行线程代码
  • 装置试错代码:增加对Object.wait()、Object.sleep()、Object.yield()、Object.priority()等方法的调用,改变代码执行顺序,硬编码或自动化

使用工具来编程实现装置代码,如 Aspect-Oriented-Framework 、CGLIB或者ASM之类工具通过编程来装置代码

10.总结

遵守SRP —> 了解并发问题的可能原因 —> 学习类库,了解基本算法 —> 学习如何找到必须锁定的代码区域并锁定之—> 问题会跳出来 —> 写装置代码,提高发现错误代码的机会

11.参考文献

《代码整洁之道》 www.jianshu.com/p/c44eca6ad… zhuanlan.zhihu.com/p/37639722

关注公众号“程序员面试之道”

回复“面试”获取面试一整套大礼包!!!

本公众号分享自己从程序员小白到经历春招秋招斩获10几个offer的面试笔试经验,其中包括【Java】、【操作系统】、【计算机网络】、【设计模式】、【数据结构与算法】、【大厂面经】、【数据库】期待你加入!!!

1.计算机网络----三次握手四次挥手

2.梦想成真-----项目自我介绍

3.你们要的设计模式来了

4.震惊!来看《这份程序员面试手册》!!!

5.一字一句教你面试“个人简介”

6.接近30场面试分享

文章分类
后端
文章标签