本文正在参与「金石方案 . 瓜分6万现金大奖」

“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界说不就好了,确实道理是这个道理,可是“木已成舟、米已成炊”,是哪位大神留下的代码啊……‍「避坑宝典」为大家分享一下笔者在 2022 年所遇到“匪夷所思”的 Bug 趣事(上)

  2. ‍接受上面的问题还引发出了许多连锁问题,比如说:’null’的操控是不简单判断出来的,比如;isEmpty/isBlank等办法无法处理,所以会将过错的数据更新进入数据库以及核算数据的时分呈现了紊乱(例如:Mybatis的if test 一般只会做!=null的判断、加解密的场景下,一般不会对null或许”的值进行处理,可是一旦呈现了此种场景,就产生了反常!)‍「避坑宝典」为大家分享一下笔者在 2022 年所遇到“匪夷所思”的 Bug 趣事(上)

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

场景评论
  1. ‍问题产生在数据库总存储Null的场景:包含了null和”的两种状况,此刻查询的时分ifnull的函数无法进行判断,能够考虑选用”的匹配或许length函数才能够进行操控!可是没想到啊!咱们的数据库同一个字段存储了两种null和”都有!此外还“发扬光大”!呈现了[],空调集的操作,并且事务场景下数据也为空,哈哈千算万算,没想到还有这一招!咱们可要留意啊。

  2. ‍多说一句哈,核算或许窗口函数的时分对null的处理一定要多留意,咱们接受’null’的时分,呈现了仅有索引的问题:MySQLIntegrityConstraintViolationException,由于’null’呈现了重复,innodb引擎是能够允许仅有索引进行多个null的场景,可是’null’或许”、[]是不允许的哦!‍「避坑宝典」为大家分享一下笔者在 2022 年所遇到“匪夷所思”的 Bug 趣事(上)

  3. 此外有一个同伴们,由于不想总是针关于null进行判断,所以将PO数据持久目标属性的默认值:导致许多数据库中的默认值都失效了,┭┮﹏┭┮。好难过!‍「避坑宝典」为大家分享一下笔者在 2022 年所遇到“匪夷所思”的 Bug 趣事(上)

区分空字符串和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。
  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精度哦,哈哈,没有外表的那么简单哦!

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

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

本文正在参与「金石方案 . 瓜分6万现金大奖」

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。