「避坑宝典」为大家分享一下笔者在 2022 年所遇到“匪夷所思”的 Bug 趣事(上)

简介: 「避坑宝典」为大家分享一下笔者在 2022 年所遇到“匪夷所思”的 Bug 趣事(上)

“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场景,

  2. NumberFormatException:"null",此部分在于'null'的场景:总结下来归咎为:代码中有个小伙伴使用了String.valueOf方法区承接参数的Integer和Long类型的转换操作,(当没有传递该参数的时候或者传递了null)导致最后数据库转为了'null',这里大家会说为什么要用String去转换或者直接用String定义不就好了,确实道理是这个道理,但是“木已成舟、米已成炊”,是哪位大神留下的代码啊......‍


  3. ‍承接上面的问题还引发出了很多连锁问题,比如说:'null'的控制是不容易判断出来的,比如;isEmpty/isBlank等方法无法处理,所以会将错误的数据更新进入数据库以及计算数据的时候出现了紊乱(例如:Mybatis的if test 一般只会做!=null的判断、加解密的场景下,一般不会对null或者''的值进行处理,但是一旦出现了此种场景,就发生了异常!)‍



「2022-05-X」空指针问题(2)


场景讨论


  1. ‍问题发生在数据库总存储Null的场景:包含了null和''的两种情况,此时查询的时候ifnull的函数无法进行判断,可以考虑采用''的匹配或者length函数才可以进行控制!但是没想到啊!我们的数据库同一个字段存储了两种null和''都有!此外还“发扬光大”!出现了[],空集合的操作,而且业务场景下数据也为空,哈哈千算万算,没想到还有这一招!大家可要注意啊。


  1. ‍多说一句哈,统计或者窗口函数的时候对null的处理一定要多注意,我们承接'null'的时候,出现了唯一索引的问题:MySQLIntegrityConstraintViolationException,因为'null'出现了重复,innodb引擎是可以允许唯一索引进行多个null的场景,但是'null'或者''、[]是不允许的哦!‍


  2. 此外有一个伙伴们,因为不想总是针对于null进行判断,所以将PO数据持久对象属性的默认值:导致很多数据库中的默认值都失效了,┭┮﹏┭┮。好难过!‍



区分空字符串和NULL


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


  • 空字符串是单引号引的,它其实是不占空间的。


  • MySQL 中 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。


  1. 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精度哦,哈哈,没有表面的那么简单哦!

image.png

那么对于这种本身就需要忽略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的问题,相信地球人都知道,笔者就不多说了,希望大家以后多多注意这种第三方库的选用哦,以后我还会多多分析一些相关的开发过程中的问题和深坑哦,敬请期待我们的(下篇)。




相关实践学习
消息队列RocketMQ版:基础消息收发功能体验
本实验场景介绍消息队列RocketMQ版的基础消息收发功能,涵盖实例创建、Topic、Group资源创建以及消息收发体验等基础功能模块。
消息队列 MNS 入门课程
1、消息队列MNS简介 本节课介绍消息队列的MNS的基础概念 2、消息队列MNS特性 本节课介绍消息队列的MNS的主要特性 3、MNS的最佳实践及场景应用 本节课介绍消息队列的MNS的最佳实践及场景应用案例 4、手把手系列:消息队列MNS实操讲 本节课介绍消息队列的MNS的实际操作演示 5、动手实验:基于MNS,0基础轻松构建 Web Client 本节课带您一起基于MNS,0基础轻松构建 Web Client
相关文章
|
9月前
|
安全 网络协议 网络安全
网络安全笔记整理,你花了多久弄明白架构设计
网络安全笔记整理,你花了多久弄明白架构设计
|
算法 网络协议 Java
起飞!阿里P8亲自撰写的这份Java最新面试手册,堪称面试作弊神器
想必最近很多朋友在为 “金九银十” 地跳槽涨薪做面试准备,作为互联网雷锋的我,从阿里P8大佬手里拿到一份堪称面试作弊神器的《Java最新面试手册》里面包含了(Java相关、Java并发编程、JVM面试题、数据结构与算法、网络协议面试题、数据库、框架相关面试题、微服务、中间件相关、Linux等等)这份面试手册可谓是面面俱到。 废话不多说,给大家展示详细内容 目录总览 下面详细给大家展示详细内容,需要这份Java最新面试手册的朋友点击此处即可! Java相关 Java并发编程 内容实在太多就不全部截图了,给大家看详细内容 JVM面试题
147 0
|
设计模式 监控 Java
简直人生外挂,直接涨薪25K,跪谢这份Java性能调优实战宝典
从整个软件生命周期的视角去关注与审视软件性能,通过主动式地设计与实现来主导软件性能,这样才能保证软件性能长期保持竞争力。针对这些问题,我结合自己十余年的性能调优开发经验。
|
存储 算法
看了齐姐这篇文章,再也不怕面试问树了(上)
在写完了所有线性数据结构之后,今天开启非线性数据结构系列。 我们今天先来看,什么是“树”
140 0
看了齐姐这篇文章,再也不怕面试问树了(上)
|
存储 安全 搜索推荐
天选程序员:如何提个好问题?
天选程序员:如何提个好问题?
284 0
天选程序员:如何提个好问题?
|
关系型数据库 MySQL 测试技术
干了3年程序员,熬夜一周整理的高性能MySQL笔记,小白也能看懂!
MySQL高性能第三版更新了大量的内容,不但涵盖了最新版MySQL的新特性以外,还讲述了关于固态盘、高可拓展性设计和云计算环境下的数据库相关的新内容,原有的基准测试和性能优化部分也做了大量的扩展和补充,全书分为16个知识点点
139 0
|
存储 前端开发 JavaScript
前端面试篇,应届生面试时被问项目经验不用慌,按这个步骤回答成功率高达95%
前端面试篇,应届生面试时被问项目经验不用慌,按这个步骤回答成功率高达95%
465 0
|
消息中间件 缓存 中间件
来自一个阿里工作的朋友经验之谈!
第一点:就是阿里的技术。
198 0
|
缓存 架构师 NoSQL
程序员面试 10 大潜规则,千万不要踩坑!
很多刚入行的小伙伴特别容易犯的一个错误,不清楚面试官到底想问什么,其实整个面试中面试官并没有想难道你的意思,只是想通过提问的方式来知道你会什么

热门文章

最新文章