Java 业务开发常见错误 100 例 -- 学习笔记

开篇词

业务的两大特点

工期紧、逻辑复杂,开发人员会更多地考虑主流程逻辑的正确实现,忽略非主流程逻辑,或保障、补偿、一致性逻辑的实现;
往往缺乏详细的设计、监控和容量规划的闭环,结果就是随着业务发展出现各种各样的事故。

看到这里我对其中的第一点感受尤为深刻,依稀记得软件工程导论项目验收的前一晚上,我们匆匆忙忙的开始测试,所有业务都可以按照我们正常的逻辑完成预期的任务,但是如果按照非主流逻辑,程序的bug就变得层出不穷,例如:电话号码的填写,前后端都没有做校验,最后就导致电话号码可以是中文而不会报错。现在再想想这种错误其实挺幼稚的,可惜当时没有开发经验,只注重了主流逻辑的正确实现。

关于第二点,我的评价是和真正生产环境的文档比起来,之前我们自己造的文档只能叫电子垃圾,相比较而言真的没什么设计可言。因此我认为自己还是要多看看大家写的文档,让自己完成从demo向工程的过渡。

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

ThreadLocal
注意线程池中线程反复使用的问题,应该使用try finally在操作结束时手动清空ThreadLocal,以防止下一次复用线程时读取到上一次的数据。
ConcorrentHashMap
在面试中也经常会问到这方面问题,比如HashMap线程线程安全吗,或者你讲讲你了解的线程安全的数据结构等等。。。
ConcorrentHashMap是一种线程安全的数据结构,但是也仅限于他进行原子性的读写时可以保证线程安全,也就是说当我们想要使用ConcurrentHashMap的size、isEmpty等方法时,可能会返回它的中间状态,从而产生并发的问题。
computeIfAbsent
HashMap和ConcurrentHashMap中都有这个方法,我的理解是首先在寻找有没有该key,如果没有就插入。感觉不太准确,后续再填坑。这个方法是通过Java底层的cas实现的,在虚拟机层面确保了写入数据的原子性,效率远高于锁。
ForkJoinPool
之前没见过,看起来像线程池的升级版,留一个坑以后再填。

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

加锁前要清楚锁和被保护的对象是不是一个层面的
只为必要的代码块加锁,可以提高程序的效率
避免死锁,可以首先从避免循环等待的方面考虑

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

为什么不推荐使用Executors类来创建线程池

  1. 我们需要根据自己的场景、并发情况来评估线程池的几个核心参数,包括核心线程数、最大线程数、线程回收策略、工作队列的类型,以及拒绝策略,确保线程池的工作行为符合需求,一般都需要设置有界的工作队列和可控的线程数(书上原话)
  2. 任何时候,都应该为自定义线程池指定有意义的名称,以方便排查问题。当出现线程数量暴增、线程死锁、线程占用大量 CPU、线程执行出现异常等问题时,我们往往会抓取线程栈。此时,有意义的线程名称,就可以方便我们定位问题(没看太懂,为什么快捷线程池就不能自定义命名了捏)

线程池的选用问题

复用线程池不代表应用程序始终使用同一个线程池,我们应该根据任务的性质来选用不同的线程池。特别注意 IO 绑定的任务和 CPU
绑定的任务对于线程池属性的偏好,如果希望减少任务间的相互干扰,考虑按需使用隔离的线程池。

看得出来这句话非常有道理,但是在我接触到的开发中确实还没有遇到这样的问题,留一个坑。

在使用线程池时要做好相应的监控工作,以免线程池崩溃造成严重损失。

###04 | 连接池:别让连接池帮了倒忙
没见过 看得一头雾水
跳过了
###05 | HTTP调用:你考虑到超时、重试、并发了吗?
看了半天,没遇到过这种问题,实在做不到感同身受,过了

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

@Transactional 注解开启声明式事务的配置问题

  1. 只有定义在public方法上的@Transactional才能生效,因为该注解默认是通过动态代理实现的,而动态代理无法代理到private方法。(一些特殊配置除外,例如:使用 AspectJ 静态织入实现 AOP,这说的啥我也没看懂)
  2. 必须通过代理过的类从外部调用目标方法才能生效

异常处理不正确而导致的问题

@Transactional 注解的方法出现了 RuntimeException 和 Error 的时候回滚,如果我们的方法捕获了异常,那么需要通过手动编码处理事务回滚

@Transactional 注解的 Propagation 属性
在多次数据库操作,并希望将它们作为独立的事务进行提交或回滚时,需要考虑事务的传播,没用过,留一个坑。

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

二次索引的额外代价

  1. 维护代价,每次插入或删除新数据时,所有涉及到的索引都要对应的发生变化
  2. 空间代价,二次索引同样会额外占用一些空间
  3. 回表代价,二次索引不保存全部数据,所以在一些情况下需要根据主键再回到聚簇索引中查询原有数据

索引失效
4. 模糊搜索时,无法对后缀匹配使用索引
5. 当查询涉及函数操作,无法通过索引查询
6. 最左匹配原则,使用联合索引时,必须包含索引最左侧的字段,同时由于查询优化器的存在,最左侧字段在where子句的第几个条件并不重要

数据库对索引的选择
MySQL根据表的IO成本(聚簇索引占用的页面数,用来计算读取数据的 IO 成本)和CPU成本(表中的记录数,用来计算搜索的 CPU 成本)来进行索引的选择,当然IO成本和CPU成本不是实时的,而是会对表的信息进行维护。当由于对表的信息统计有误或者估算不准确时,我们也可以强制使用某个索引

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

注意 equals 和 == 的区别
==比较基本类型
equals比较引用类型(指针)
JVM的[-128,127]
JVM字符串常量池机制
在使用自定义类型时,注意保证equals、hashcode、和compareTo的一致性
Lombok 的 @EqualsAndHashCode 注解实现 equals 和 hashCode 的时候,默认使用类型所有非 static、非 transient 的字段,且不考虑父类,可以使用 @EqualsAndHashCode.Exclude 排除一些字段,并设置 callSuper = true 来让子类的 equals 和 hashCode 调用父类的相应方法。(还没碰见过)

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

浮点数的精确表达—BigDecimal
当我们用float和double是,结果难以避免的会出现精度缺失的情况,如果想要对浮点数进行准确的表示和运算,我们就需要使用BigDecimal对象。
BigDecimal是怎么来保证精度的呢?其实非常的简单朴素,浮点数转化为二进制会存在精度缺失,但是十进制整数不会啊,索引我们只需要将浮点数扩大一定的倍数,在整数范畴进行运算,就可以有效地避免精度缺失问题。
浮点数避坑-注意事项

  1. 用 BigDecimal 表示和计算浮点数,且务必使用字符串的构造方法来初始化 BigDecimal
    具体是为什么呢?不知道,没搜到,留坑+1
System.out.println(new BigDecimal(0.1).add(new BigDecimal(0.2)));//不精确
System.out.println(new BigDecimal("0.1").add(new BigDecimal("0.2")));//精确

如果只有一个Double对象,如何转化成BigDecimal呢?
——直接使用Double.toString(×)
BigDecimal 有 scale 和 precision 的概念,scale 表示小数点右边的位数,而 precision 表示精度,也就是有效数字的长度。
使用Double.toString和直接传入字符串获取到的scale和precision的值是不同的,所以会导致两种情况的精度不同。书上有具体的实例诠释,这里就不复制了。
如果一定要用 Double 来初始化 BigDecimal 的话,可以使用 BigDecimal.valueOf 方法,以确保其表现和字符串形式的构造方法一致。
2. 浮点数的字符串格式化也要通过 BigDecimal 进行
简单来说就是由于精度缺失,有时候浮点数的舍入会出现不符合预期的结果,所以需要用到BigDecimal,他也自带了一些舍入方式

BigDecimal num1 = new BigDecimal("3.35");
BigDecimal num2 = num1.setScale(1, BigDecimal.ROUND_DOWN);
System.out.println(num2);//3.3
BigDecimal num3 = num1.setScale(1, BigDecimal.ROUND_HALF_UP);
System.out.println(num3);//3.4

3.进行数值运算时要小心溢出问题,虽然溢出后不会出现异常,但得到的计算结果是完全错误的。
这么看下来其实还是有一点晕,反正总而言之就一句话,在金融、科学计算等场景,请尽可能使用 BigDecimal 和 BigInteger

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

使用 Arrays.asList 把数据转换为 List 的三个坑

  1. 不能直接使用 Arrays.asList 来转换基本类型数组
//失败案例
int[] arr = {1, 2, 3};
List list = Arrays.asList(arr);

//正确方法1:使用Arrays.stream,jdk版本>8
int[] arr1 = {1, 2, 3};
List list1 = Arrays.stream(arr1).boxed().collect(Collectors.toList());

//正确方式2:声明为Integer数组
Integer[] arr2 = {1, 2, 3};
List list2 = Arrays.asList(arr2);
  1. Arrays.asList 返回的 List 不支持增删操作
    因为这里返回的List是Arrays的内部类ArrayList而不是我们熟知的java.util.ArrayList

  2. 对原始数组的修改会影响到我们获得的那个 List,解决方法也很简单,再新建一个ArrayList就可以完成分离。

List list = new ArrayList(Arrays.asList(arr));

使用 List.subList 进行切片操作导致的OOM
这就要从subList的实现逻辑说起,subList不是真正的复制了链表,而仅仅是原始链表的一个视图,例如十万元素的链表分割出来一小部分,实际上还是保存了十万个元素。同时对分割出的链表进行操作,也会和原始的链表相互影响。

一定要让合适的数据结构做合适的事情

  • HashMap
    HashMap有一个特性,就是它的检索效率为O(1),所以如果需要在超大的ArrayList中检索数据时,可以考虑使用HashMap来优化性能,但是同时也会付出在空间上的代价。
  • LinkedList
    不建议使用
    在插入和删除使用比较多的链表中可能有的同学会想到用LinkedList,但是插入时间复杂度为O(1)的前提是,我们已经获得到了要插入的地方的指针,而获取指针的时间复杂度又会是O(n)。经过综合测试,LinkedList在各个环境下的性能都很难比上ArrayList,即使它的创作者也不推荐使用LinkedList。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值