“BUG”的你咋又来了?

无论这一年我们遇到了什么困难或者是喜事,在此时此刻,个人觉得都应该反思或者回味一下这些事情,对于好事,我们欣慰开心;坏事那我们能做到的就是极力避免它们再次发生,就如同接下来笔者要介绍的整个熟悉而陌生的名称“BUG”,接下来我主要会为大家介绍一下,发生在2022年这一年中的笔者在开发过程中所遇到的“bug”和“坑”。

衷心提醒

希望大家不要当做笑话,认真去了解或者研究笔者所梳理出来的坑和bug,希望可以警示和告诫大

家,无论在代码的书写层面还是实际的开发层面都可以跳出这些问题改圈和坑点!在开发的航线中一路顺风,成为IT界的“海贼王”!

直奔主题空指针的问题系列「2022-03-X」空指针问题场景讨论
  1. 问题发生在Boolean类型的判断场景
  2. ‍问题发生在Integer类型参数传递给方法参数或者局部变量时候的自动拆箱场景
  3. ‍问题发生在对象中采用了List集合字段以及Map字段的时候进行操作,一般我们很少会对List或者HashMap参数进行初始化在进行赋值,此时出现了获取了一个null的引用进行add或者put操作。
「2022-05-X」空指针问题(1)场景讨论
  1. 问题发生在相关业务场景的Null值操作,最后暴露在非传统的NPE场景,NumberFormatException:"null",此部分在于'null'的场景:总结下来归咎为:代码中有个小伙伴使用了String.valueOf方法区承接参数的Integer和Long类型的转换操作,(当没有传递该参数的时候或者传递了null)导致最后数据库转为了'null',这里大家会说为什么要用String去转换或者直接用String定义不就好了,确实道理是这个道理,但是“木已成舟、米已成炊”,是哪位大神留下的代码啊......‍
  2. ‍承接上面的问题还引发出了很多连锁问题,比如说:'null'的控制是不容易判断出来的,比如;isEmpty/isBlank等方法无法处理,所以会将错误的数据更新进入数据库以及计算数据的时候出现了紊乱(例如:Mybatis的if test 一般只会做!=null的判断、加解密的场景下,一般不会对null或者''的值进行处理,但是一旦出现了此种场景,就发生了异常!)‍
「2022-05-X」空指针问题(2)场景讨论
  1. ‍问题发生在数据库总存储Null的场景:包含了null和''的两种情况,此时查询的时候ifnull的函数无法进行判断,可以考虑采用''的匹配或者length函数才可以进行控制!但是没想到啊!我们的数据库同一个字段存储了两种null和''都有!此外还“发扬光大”!出现了[],空集合的操作,而且业务场景下数据也为空,哈哈千算万算,没想到还有这一招!大家可要注意啊。
  2. ‍多说一句哈,统计或者窗口函数的时候对null的处理一定要多注意,我们承接'null'的时候,出现了唯一索引的问题:MySQLIntegrityConstraintViolationException,因为'null'出现了重复,innodb引擎是可以允许唯一索引进行多个null的场景,但是'null'或者''、[]是不允许的哦!‍
  3. 此外有一个伙伴们,因为不想总是针对于null进行判断,所以将PO数据持久对象属性的默认值:导致很多数据库中的默认值都失效了,┭┮﹏┭┮。好难过!‍
区分空字符串和NULL

数据库存储数据有必要搞清空值,空字符串和 NULL 的概念。

null其实并不是空值,而是要占用空间,所以 MySQL在进行比较的时候,null会参与字段比较,所以对效率有一部分影响。对于表索引时不会存储null值的,所以如果索引的字段可以为null,索引的效率会下降很多。

空值也不一定为空,对于timestamp数据类型,如果往这个数据类型插入的列插入 null 值,则出现的值是当前系统时间,插入空值,则会出现 ‘0000-00-00 00:00:00’。

根据NULL的定义,NULL表示的是未知,因此两个NULL比较的结果既不相等,也不不等,结果仍然是未知。根据这个定义,多个NULL值的存在应该不违反唯一约束,所以是合理的,在oracle也是如此。

集合的问题系列「2022-03-X」集合问题场景讨论

集合转换问题:用Array.asList转换基础类型数组,此时转换后的List集合的元素是有问题的,当接收页面请求的时候,循环以及获取元素的时候程序崩溃了!

int[] arr = {1, 2, 3}; List list = Arrays.asList(arr); log.info("list:{} size:{} class:{}", list, list.size(), list.get(0).getClass()); 复制代码

‍此时List集合的长度并不是我们预期的3,而是1,因为内部的元素是一个数组,而不是所有的元

素。‍直接遍历这样的List必然会出现 Bug:

public static <T> List<T> asList(T... a) { return new ArrayList<>(a); } 复制代码

解决方案

如果使用java8以上版本可以使用 Arrays.stream 方法来转换,否则可以把 int 数组声明为包装类型 Integer 数组:

int[] arr1 = {1, 2, 3}; List list1 = Arrays.stream(arr1).boxed().collect(Collectors.toList()); List list2 = Arrays.asList(arr1); 复制代码

不能直接使用Arrays.asList来转换基本类型数组。

集合转换问题:Arrays.asList 返回的List不支持增删操作

String[] arr = {"1", "2", "3"}; List list = Arrays.asList(arr); arr[1] = "4"; try { list.add("5"); } catch (Exception ex) { ex.printStackTrace(); } 复制代码

原因分析:Arrays.asList返回的List不支持增删操作。Arrays.asList 返回的 List 并不是我们期望的 java.util.ArrayList,而是 Arrays 的内部类 ArrayList。ArrayList 内部类继承自 AbstractList 类,并没有覆写父类的 add 方法,而父类中 add 方法的实现,就是抛出 UnsupportedOperationException。

private static class ArrayList<E> extends AbstractList<E> implements RandomAccess, java.io.Serializable 复制代码

集合转换问题:对原始数组的修改会直接影响得到的list‍

String[] arr = {"1", "2", "3"}; List<String> list = Arrays.asList(arr); arr[0]="aaaaa"; 复制代码

asList生成的那个Array内部的ArrayList内部直接使用了原始的array导致的,这估计也是不让生成的list add和remove的原因吧,因为这样会影响到原始值。

「2022-03-X」集合问题(2)场景考虑

List.subList操作还会导致OOM?

‍在日常开发过程中,经常会常常需要取集合中的某一部分子集来进行一下操作,而对于subList这个方法会经常的被我们所熟知。

List<Object> lists = new ArrayList<Object>(); lists.add( "1" ); lists.add( "2" ); lists.add( "3" ); lists.add( "4" ); ... ... List<Object> tempList = lists.subList( 2 , lists.size()); 复制代码

在上面的代码终会,执行的subList方法的次数越多、或者分离的原始集合越大,越容易出现OOM,我们其实很容易误解,底层真正会对数组或者List集合进行相关的分割,其实不然,本身来讲会建立的方案只是单纯的逻辑分割哦!让我们来看看为什么会出现。

原因分析

‍假设来100000次循环中的产生的一个个size为1000的list始终执行subList。那么返回的List强引用,使他得不到回收造成的。接下来我们来看一看为什么返回的子list会强引用原来的list。我们点进入ArrayList.subList()的源码。

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { transient Object[] elementData; private int size; private void add(E e, Object[] elementData, int s) { if (s == elementData.length) elementData = grow(); elementData[s] = e; size = s 1; } public boolean add(E e) { modCount ; add(e, elementData, size); return true; } public List<E> subList(int fromIndex, int toIndex) { subListRangeCheck(fromIndex, toIndex, size); return new SubList<>(this, fromIndex, toIndex); } private static class SubList<E> extends AbstractList<E> implements RandomAccess { private final ArrayList<E> root; private final SubList<E> parent; private final int offset; private int size; public SubList(ArrayList<E> root, int fromIndex, int toIndex) { this.root = root; this.parent = null; this.offset = fromIndex; this.size = toIndex - fromIndex; this.modCount = root.modCount; } public boolean contains(Object o) { return indexOf(o) >= 0; } public Iterator<E> iterator() { return listIterator(); } public ListIterator<E> listIterator(int index) { checkForComodification(); rangeCheckForAdd(index); ... } private void checkForComodification() { if (root.modCount != modCount) throw new ConcurrentModificationException(); } } } 复制代码

SubList类的构造方法:

public SubList(ArrayList<E> root, int fromIndex, int toIndex) { this.root = root; this.parent = null; this.offset = fromIndex; this.size = toIndex - fromIndex; this.modCount = root.modCount; } 复制代码

  1. subList()返回的并不是一个ArrayList,他返回的是一个SubList类,并且在初始化时传入了this。
  2. SubList是ArrayList的一个内部类。再看一下他的构造方法会发现他的root就是原来的List,初始化时并没有将截取的元素复制到新的变量中。由此可见SubList就是原来List的视图,并不是新的List,双方对集合中元素的修改是会互相影响的。并且因为SubList对原来的List有强引用,导致这些原始集合不能被垃圾回收,所以导致了OOM。
  3. SubList的构造方法中我们会发现this.modCount = root.modCount,SubList的modCount就是原来集合的modCount。modCount是在ArrayList中维护的一个字段,表示集合的结构性修改的次数。所以对于原始集合的add,remove操作时一定会改变原始集合modCount的值,而经过subList()后得到的List的modCount是不会改变的。
解决方案‍

List.subList操作导致OOM的根本原因就是分片后的List对饮食集合的强引用。为了避免这种情况的发生,在获取到分片后的List后,我们不要直接使用这个集合进行操作,可以使用一个新的变量保存分片后的list。

// 方法一 List<Integer> arrayList = new ArrayList<>(rawList.subList(0, 2)); // 方法二 List<Integer> arrayList1 = list.stream().skip(1).limit(3).collect(Collectors.toList()); 复制代码

因为sublist中保存有原有list对象的引用——而且是强引用,这意味着, 只要sublist没有被jvm回收,那么这个原有list对象就不能gc,这个list中保存的所有对象也不能gc,即使这个list和其包含的对象已经没有其他任何引用。

数值计算的问题系列「2022-03-X」计算问题场景考虑:

Double和Float的计算操作,加减乘除方式会存在相关的误差哦,初级小伙伴们,一定要注意,如果

(1)要求比较高一定要采用BigDecimal类型进行计算操作。‍

String a = "16.11"; Double v = Double.parseDouble(a) * 100; BigDecimal bigDecimal = new BigDecimal(a); BigDecimal multiply = bigDecimal.multiply(new BigDecimal(100)); 复制代码

最后的结果是会存在误差的哦,Double的数据会<1611。

(2)条件判断超预期

System.out.println( 1f == 0.9999999f ); // 打印:false** System.out.println( 1f == 0.99999999f ); // 打印:true 惊喜不? 复制代码

最后的比较大小会存在歧义,差一位小数,竟然天壤之别

(3)数据转换超预期

float f = 1.1f; double d = (double) f; System.out.println(f); // 打印:1.1 System.out.println(d); // 打印:1.100000023841858,咋会变成这样 复制代码

「2022-11-X」计算问题场景考虑:

你以为BigDecimal就没有坑了?它的精度与相等比较的坑(equals方法可能不相等)

作为一个数字类型,经常有的操作是比较大小,有一种情况是比较是否相等。用equal方法还是compareTo方法?这里就是一个大坑。

//new 传进去一个double BigDecimal newZero = new BigDecimal(0.0); System.out.println(BigDecimal.ZERO.equals(newZero)); //new 传进去一个字符串 BigDecimal stringNewZero = new BigDecimal("0.0"); System.out.println(BigDecimal.ZERO.equals(stringNewZero)); //valueOf 传进去一个double BigDecimal noScaleZero = BigDecimal.valueOf(0.0); System.out.println(BigDecimal.ZERO.equals(noScaleZero)); //valueOf 传进去一个double,再手动设置精度为1 BigDecimal scaleZero = BigDecimal.valueOf(0.0).setScale(1); System.out.println(BigDecimal.ZERO.equals(scaleZero)); 复制代码

用于比较的值全都是0,猜一猜上面几个equals方法返回的结果是什么?全都是true?no no no...

true false false false 复制代码

看看equal方法你就会豁然开朗咯,它还比较scale精度哦,哈哈,没有表面的那么简单哦!

黑坑常用名词解释图解(为大家分享笔者在22)(1)

那么对于这种本身就需要忽略scale的对比怎么办?其实BigDecimal类也提供了相关的compare方法,而且这个方法的设计也和comparable接口的实现也很相似,所以使用起来也挺舒服的。

public int compareTo(BigDecimal val) { // Quick path for equal scale and non-inflated case. if (scale == val.scale) { long xs = intCompact; long ys = val.intCompact; if (xs != INFLATED && ys != INFLATED) return xs != ys ? ((xs > ys) ? 1 : -1) : 0; } int xsign = this.signum(); int ysign = val.signum(); if (xsign != ysign) return (xsign > ysign) ? 1 : -1; if (xsign == 0) return 0; int cmp = compareMagnitude(val); return (xsign > 0) ? cmp : -cmp; } 复制代码

一个更大的坑是,如果将BigDecimal的值作为HashMap的key,因为精度的问题,相同的值就可能出现hashCode值不同并且equals方法返回false,导致put和get就很可能会出现相同的值但是存取了不同的value。小数类型在计算机中本来就不能精确存储,再把其作为HashMap的key就相当不靠谱了,以后还是少用。

比较的问题系列lombok中注解@EqualsAndHashCode的坑

@Data相当于@Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode这5个注解的合集

//父类 @Data public class Parent { private String id;} //子类 @Data public class Child extends Parent { private String name;} 复制代码

所以如果继承父类时候使用@Data需要加上@EqualsAndHashCode(callSuper = true),如下:

@Data @EqualsAndHashCode(callSuper = true) public class Child extends Parent { private String name; } 复制代码

并发多线程的问题「2022-10-X」数据紊乱ThreadLocal问题场景考虑:

在登录认证后,我们系统频繁高并发去处理请求的时候,发现数据出现了紊乱,什么紊乱?就是多个账号之间的数据发生了流窜,道理很简单从数据上来看就是数据对应的userId完全对不上了。

分析了以后发现,系统在调用的时候对ThreadLocal的使用出现了内存泄漏以及内存数据紊乱的问

题,也就是和PageHelper一样的道理,需要清理参数执行,在公司内部的系统中出现了相关的权限认证和会话信息注入到ThreadLocal的内容,这个相信大家并不陌生,但是在有一些不需要鉴权的接口的时候,就会存在不会处理ThreadLocal中数据的remove以及更新的操作,导致出现了数据紊乱的问题。

解决方案

web请求下的ThreadLocal使用要保证:请求进来的时候set,请求回去的时候remove。只有这样才能保证请求内的ThreadLocal 是唯一的。 这个特性在深刻的提醒我们:一次http请求和tomcat启动处理业务的线程并非一一对应的,而是通过一个线程池进行调度。

「2022-09-X」ConcurrentHashMap的线程不安全问题案例分析

ConcurrentHashMap是个线程安全的哈希表容器,但它仅保证提供的原子性读写操作线程安全。

当我们在通过多线程情况下,如果在对相关的ConcurrentHashMap做较为复杂的操作处理功能的时候,就会存在线程不安全的场景:

map.put(1,getResult()); 这种场景就是线程不安全的考虑哦!请大家慎用和谨记!

ConcurrentHashMap对外提供能力的限制:

  • 使用不代表对其的多个操作之间的状态一致,是没有其他线程在操作它的。如果需要确保需要手动加锁
  • 诸如size、isEmpty和containsValue等聚合方法,在并发下可能会反映ConcurrentHashMap的中间状态。因此在并发情况下,这些方法的返回值只能用作参考而不能用于流程控制
  • 诸如putAll这样的聚合方法也不能确保原子性,在putAll的过程中去获取数据可能会获取到部分数据。
解决方案‍

我们可以使用相关的computeIfAbsent、putIfAbsent等操作可以保证原子化处理。

可以参考一下这篇文章哦:blog.csdn.net/singwhatiwa…

RocketMQ问题分析系列场景考虑:

发送Topic消息报该错误,com.alibaba.rocketmq.client.exception.MQBrokerException: CODE: 2 DESC: [TIMEOUT_CLEAN_QUEUE]broker busy, start flow control for a while, period in queue: 208ms, size of queue: 8

sendThreadPoolQueue取出头元素,转换成对应的任务,判断任务在队列存活时间是否超过了队列设置的最大等待时间,如果超过了则组装处理返回对象response,response的code为RemotingSysResponseCode.SYSTEM_BUSY。

[TIMEOUT_CLEAN_QUEUE]broker busy, start flow control for a while, period in queue: [当前任务在队列存活时间], size of queue: [当前队列的长度]

解决方案

说实在的就是RocketMQ处理不过来了:那么有以下几个选择供大家参考:

  1. 给rocketmq单独部署性能较高的服务器.
  2. ‍sendMessageThreadPoolNums 改成 N(N>1),useReentrantLockWhenPutMessage改成true,修改broker的默认发送消息任务队列等待时长waitTimeMillsInSendQueue,可通过增大 osPageCacheBusyTimeOutMills进一步优化调整,仅供参考,不是万金油,会有副作用的哦,慎用!
结束语

好了就到这里了,已经接近8000字了,笔者最后就是提醒大家,最近的Log4j2的问题,相信地球人都知道,笔者就不多说了,希望大家以后多多注意这种第三方库的选用哦,以后我还会多多分析一些相关的开发过程中的问题和深坑哦,敬请期待我们的(下篇)。

,