Java业务开发常见错误100例------①代码篇 (23讲)

01 | 使用了并发工具类库,线程安全就高枕无忧了吗?

  • ThreadLocal 使用的注意点
  • ConcurrentHashMap 只能保证提供的原子性读写操作是线程安全的。符合操作时需要注意原子性
    • 使用了 ConcurrentHashMap,不代表对它的多个操作之间的状态是一致的,是没有其他线程在操作它的,如果需要确保需要手动加锁。
    • 诸如 size、isEmpty 和 containsValue 等聚合方法,在并发情况下可能会反映 ConcurrentHashMap 的中间状态。因此在并发情况下,这些方法的返回值只能用作参考,而不能用于流程控制。显然,利用 size 方法计算差异值,是一个流程控制。
    • 诸如 putAll 这样的聚合方法也不能确保原子性,在 putAll 的过程中去获取数据可能会获取到部分数据。 加锁+ 双重校验
    • ConcurrentHashMap 提供了一些原子性的简单复合逻辑方法,用好这些方法就可以发挥其威力。这就引申出代码中常见的另一个问题:在使用一些类库提供的高级工具类时,开发人员可能还是按照旧的方式去使用这些新类,因为没有使用其特性,所以无法发挥其威力。
    • 使用 ConcurrentHashMap 的原子性方法 computeIfAbsent 来做复合逻辑操作(底层CAS) ,判断 Key 是否存在 Value,如果不存在则把 Lambda 表达式运行后的结果放入 Map 作为 Value,也就是新创建一个 LongAdder 对象,最后返回 Value。
  • CopyOnWriteArrayList 来缓存大量的数据,读多写少或者说希望无锁读的场景。

思考与讨论

  1. 用到了 ThreadLocalRandom,你觉得是否可以把它的实例设置到静态变量中,在多线程情况下重用呢?(不可以)
  2. ConcurrentHashMap 还提供了 putIfAbsent 方法,你能否通过查阅JDK 文档,说说 computeIfAbsent 和 putIfAbsent 方法的区别
    • computeIfAbsent和putIfAbsent区别是三点:1、当Key存在的时候,如果Value获取比较昂贵的话,putIfAbsent就白白浪费时间在获取这个昂贵的Value上(这个点特别注意)2、Key不存在的时候,putIfAbsent返回null,小心空指针,而computeIfAbsent返回计算后的值3、当Key不存在的时候,putIfAbsent允许put null进去,而computeIfAbsent不能,之后进行containsKey查询是有区别的(当然了,此条针对HashMap,ConcurrentHashMap不允许put null value进去)

02 | 代码加锁:不要让“锁”事成为烦心事

  • synchronized 锁的对象是啥呢?场景?加锁要考虑锁的粒度和场景问题,如果精细化考虑了锁应用范围后,性能还无法满足需求的话,我们就要考虑另一个维度的粒度问题了,即:区分读写场景以及资源的访问冲突,考虑使用悲观方式的锁还是乐观方式的锁。
  • 锁的范围考虑之后在考虑锁的使用业务场景,锁的粒度和业务场景;(乐观,悲观;读锁,写锁;公平,非公平锁等)
    • 对于读写比例差异明显的场景,考虑使用 ReentrantReadWriteLock 细化区分读写锁,来提高性能。
    • 如果你的 JDK 版本高于 1.8、共享资源的冲突概率也没那么大的话,考虑使用 StampedLock 的乐观读的特性,进一步提高性能。
    • JDK 里 ReentrantLock 和 ReentrantReadWriteLock 都提供了公平锁的版本,在没有明确需求的情况下不要轻易开启公平锁特性,在任务很轻的情况下开启公平锁可能会让性能下降上百倍。
  • 多把锁要小心死锁问题,打破死锁有多种方式,根据实际业务使用即可
    • 案例:下单操作需要锁定订单中多个商品的库存,拿到所有商品的锁之后进行下单扣减库存操作,全部操作完成之后释放所有的锁。代码上线后发现,下单失败概率很高,失败后需要用户重新下单,极大影响了用户体验,还影响到了销量。解决:对购物车的商品进行排序来实现有顺序的加锁,避免循环等待。

先考虑锁的是什么?用悲观锁还是用乐观锁,读写锁之类的?多把锁锁的又是什么?会不会发生死锁的情况?业务场景合适用哪种锁好些??等等问题
在这里插入图片描述

03 | 线程池:业务代码最常用也最容易犯错的组件

  • 线程池的声明需要手动进行
  • 线程池线程管理策略详解
  • 务必确认清楚线程池本身是不是复用的
  • 需要仔细斟酌线程池的混用策略
    • 对于执行比较慢、数量不大的 IO 任务,或许要考虑更多的线程数,
    • 而不需要太大的队列。而对于吞吐量较大的计算型任务,线程数量不宜过多,可以是 CPU 核数或核数 *2(理由是,线程一定调度到某个 CPU 进行执行,如果任务本身是 CPU 绑定的任务,那么过多的线程只会增加线程切换的开销,并不能提升吞吐量),但可能需要较长的队列来做缓冲。

在这里插入图片描述

04 | 连接池:别让连接池帮了倒忙

连接池一般对外提供获得连接、归还连接的接口给客户端使用,并暴露最小空闲连接数、最大连接数等可配置参数,在内部则实现连接建立、连接心跳保持、连接管理、空闲连接回收、连接可用性检测等功能。连接池的结构示意图,如下所示:
在这里插入图片描述

  • 连接池和连接分离的 API:有一个 XXXPool 类负责连接池实现,先从其获得连接 XXXConnection,然后用获得的连接进行服务端请求,完成后使用者需要归还连接。通常,XXXPool 是线程安全的,可以并发获取和归还连接,而 XXXConnection 是非线程安全的。对应到连接池的结构示意图中,XXXPool 就是右边连接池那个框,左边的客户端是我们自己的代码。
  • 内部带有连接池的 API:对外提供一个 XXXClient 类,通过这个类可以直接进行服务端请求;这个类内部维护了连接池,SDK 使用者无需考虑连接的获取和归还问题。一般而言,XXXClient 是线程安全的。对应到连接池的结构示意图中,整个 API 就是蓝色框包裹的部分。
  • 非连接池的 API:一般命名为 XXXConnection,以区分其是基于连接池还是单连接的,而不建议命名为 XXXClient 或直接是 XXX。直接连接方式的 API 基于单一连接,每次使用都需要创建和断开连接,性能一般,且通常不是线程安全的。对应到连接池的结构示意图中,这种形式相当于没有右边连接池那个框,客户端直接连接服务端创建连接。
    在这里插入图片描述
    思考与讨论
  1. 有了连接池之后,获取连接是从连接池获取,没有足够连接时连接池会创建连接。这时,获取连接操作往往有两个超时时间:一个是从连接池获取连接的最长等待时间,通常叫作请求连接超时 connectRequestTimeout 或连接等待超时 connectWaitTimeout;一个是连接池新建 TCP 连接三次握手的连接超时,通常叫作连接超时 connectTimeout。针对 JedisPool、Apache HttpClient 和 Hikari 数据库连接池,你知道如何设置这 2 个参数吗?
  2. 对于带有连接池的 SDK 的使用姿势,最主要的是鉴别其内部是否实现了连接池,如果实现了连接池要尽量复用 Client。对于 NoSQL 中的 MongoDB 来说,使用 MongoDB Java 驱动时,MongoClient 类应该是每次都创建还是复用呢?你能否在官方文档中找到答案呢?

05 | HTTP调用:你考虑到超时、重试、并发了吗?

  • 配置连接超时和读取超时参数的学问
    • 连接超时参数 ConnectTimeout,让用户配置建连阶段的最长等待时间;
    • 读取超时参数 ReadTimeout,用来控制从 Socket 上读取数据的最长等待时间。
  • 连接超时参数和连接超时的误区有这么两个
  • 读取超时参数和读取超时则会有更多的误区
  • 并发限制了爬虫的抓取能力

在这里插入图片描述

06 | 20%的业务代码的Spring声明式事务,可能都没处理正确

  • 小心 Spring 的事务可能没有生效
  • 事务即便生效也不一定能回滚
  • 请确认事务传播配置是否符合自己的业务逻辑

在这里插入图片描述

答疑篇:代码篇思考题集锦(一)—对上面6讲的课后思考题解答


07 | 数据库索引:索引并不是万能药

  • InnoDB 是如何存储数据的?
  • 考虑额外创建二级索引的代价,创建二级索引的代价,主要表现在维护代价、空间代价和回表代价三个方面。
  • 不是所有针对索引列的查询都能用上索引
  • 数据库基于成本决定是否走索引

在这里插入图片描述

08 | 判等问题:程序里如何确定你就是你?

  • 注意 equals 和 == 的区别
  • 实现一个 equals 没有这么简单
  • hashCode 和 equals 要配对实现
  • 注意 compareTo 和 equals 的逻辑一致性,binarySearch 方法内部调用了元素的 compareTo 方法进行比较,我再强调下,对于自定义的类型,如果要实现 Comparable,请记得 equals、hashCode、compareTo 三者逻辑一致

在这里插入图片描述

09 | 数值计算:注意精度、舍入和溢出问题

  • “危险”的 Double
  • 考虑浮点数舍入和格式化的方式
  • 用 equals 做判等,就一定是对的吗?
  • 小心数值溢出问题

在这里插入图片描述

10 | 集合类:坑满地的List列表操作

  • 使用 Arrays.asList 把数据转换为 List 的三个坑
  • 使用 List.subList 进行切片操作居然会导致 OOM?
  • 一定要让合适的数据结构做合适的事情

在这里插入图片描述

11 | 空值处理:分不清楚的null和恼人的空指针

  • 修复和定位恼人的空指针问题
    • NullPointerException 是 Java 代码中最常见的异常,我将其最可能出现的场景归为以下 5 种:
    • 参数值是 Integer 等包装类型,使用时因为自动拆箱出现了空指针异常;
    • 字符串比较出现空指针异常;
    • 诸如 ConcurrentHashMap 这样的容器不支持 Key 和 Value 为 null,强行 put null 的 Key 或 Value 会出现空指针异常;
    • A 对象包含了 B,在通过 A 对象的字段获得 B 之后,没有对字段判空就级联调用 B 的方法出现空指针异常;
    • 方法或远程服务返回的 List 不是空而是 null,没有进行判空就直接调用 List 的方法出现空指针异常。
  • 在这里,我推荐使用阿里开源的 Java 故障诊断神器 Arthas。Arthas 简单易用功能强大,可以定位出大多数的 Java 生产问题。
  • POJO 中属性的 null 到底代表了什么?
  • 小心 MySQL 中有关 NULL 的三个坑

在这里插入图片描述

12 | 异常处理:别让自己在出问题的时候变为瞎子

  • 捕获和处理异常容易犯的错
  • 总之,如果你捕获了异常打算处理的话,除了通过日志正确记录异常原始信息外,通常还有三种处理模式:
    • 转换,即转换新的异常抛出。对于新抛出的异常,最好具有特定的分类和明确的异常消息,而不是随便抛一个无关或没有任何信息的异常,并最好通过 cause 关联老异常。
    • 重试,即重试之前的操作。比如远程调用服务端过载超时的情况,盲目重试会让问题更严重,需要考虑当前情况是否适合重试。
    • 恢复,即尝试进行降级处理,或使用默认值来替代原始数据。
  • 小心 finally 中的异常,包装一下或者try catch 一下
  • 万别把异常定义为静态变量
  • 提交线程池的任务出了异常会怎么样?
    • 以 execute 方法提交到线程池的异步任务,最好在任务内部做好异常处理;
    • 设置自定义的异常处理程序作为保底,比如在声明线程池时自定义线程池的未捕获异常处理程序

在这里插入图片描述

答疑篇:代码篇思考题集锦(二)----对 7~12 课后思考的解答


13 | 日志:日志记录真没你想象的那么简单

  • 为什么我的日志会重复记录?
  • 使用异步日志改善性能的坑
  • 使用日志占位符就不需要进行日志级别判断了?

14 | 文件IO:实现高效正确的文件读写并非易事

  • 文件读写需要确保字符编码一致
  • 使用 JDK1.7 推出的 Files 类的 readAllLines 方法,可以很方便地用一行代码完成文件内容读取, 但这种方式有个问题是,读取超出内存大小的大文件时会出现 OOM。为什么呢?
  • 使用 Files 类静态方法进行文件操作注意释放文件句柄
  • 注意使用 try-with-resources 方式来配合,确保流的 close 方法可以调用释放资源。
  • 注意读写文件要考虑设置缓冲区

在这里插入图片描述

15 | 序列化:一来一回你还是原来的你吗?

  • 序列化和反序列化需要确保算法一致
  • 注意 Jackson JSON 反序列化对额外字段的处理
  • 反序列化时要小心类的构造方法
  • 枚举作为 API 接口参数或返回值的两个大坑

在这里插入图片描述

16 | 用好Java 8的日期时间类,少踩一些“老三样”的坑

思维导图

在这里插入图片描述

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值