Java开发常见错误--笔记

118 阅读11分钟
  1. ThreadLocal ThreadLocal线程间存放数据, 线程的重用会导致数据的错乱。 案例分析:程序运行在 Tomcat 中,执行程序的线程是 Tomcat 的工作线程,而 Tomcat 的工作线程是基于线程池的。顾名思义,线程池会重用固定的几个线程,一旦线程重用,那么很可能首次从 ThreadLocal 获取的值是之前其他用户的请求遗留的值。这时,ThreadLocal 中的用户信息就是其他用户的信息

2.ConcurrentHashMap 理解高级API的特性 才能充分发挥作用

一种高性能的线程安全哈希表容器,但并非是一定是线程安全的,因为 ConcurrentHashMap 只能保证提供的原子性读写操作是线程安全的。 ConcurrentHashMap: 对外提供的限制和能力

  1. 使用了 ConcurrentHashMap,不代表对它的多个操作之间的状态是一致的,是没有其他线程在操作它的,如果需要确保需要手动加锁。

  2. 诸如 size、isEmpty 和 containsValue 等聚合方法,在并发情况下可能会反映 ConcurrentHashMap 的中间状态。因此在并发情况下,这些方法的返回值只能用作参考,而不能用于流程控制。显然,利用 size 方法计算差异值,是一个流程控制。

  3. 诸如 putAll 这样的聚合方法也不能确保原子性,在 putAll 的过程中去获取数据可能会获取到部分数据。

  4. CopyOnWriteArrayList 认清工具的使用场景
    读多写少或者说希望无锁读的场景 在 Java 中,CopyOnWriteArrayList 虽然是一个线程安全的 ArrayList,但因为其实现方式是,每次修改数据时都会复制一份数据出来,所以有明显的适用场景,即读多写少或者说希望无锁读的场景

    容器:ConcurrentHashMap、ConcurrentSkipListMap、CopyOnWriteArrayList、ConcurrentSkipListSet

    同步器:CountDownLatch、Semaphore、CyclicBarrier、Phaser、Exchanger

  5. 加锁

    1. 加锁前要清楚锁和被保护的对象是不是一个层面的。
    2. 加锁要考虑锁的粒度和场景问题, 尽可能的降低加锁的粒度,以及读写场景,资源的访问方式,考虑使用悲观锁还是乐观锁。
    3. 多锁问题 要小心是否是可能出现死锁问题。
  6. 线程池

池化技术 线程池,连接池,内存池 1. 线程池的声明需要手动进行new ThreadPoolExecutor 来创建线程池 2. 我们需要根据自己的场景,并发情况来评估线程池的几个核心参数,包括核心线程数,最大线程数,线程回收策略,工作队列的类型,以及拒绝策略 3.自定义线程池应该 指定有意义的名称,方便排查问题

  1. 连接池 连接池一般对外提供获得连接、归还连接的接口给客户端使用,并暴露最小空闲连接数、最大连接数等可配置参数,在内部则实现连接建立、连接心跳保持、连接管理、空闲连接回收、连接可用性检测等功能。 经常用到的连接池:数据库连接池,redis连接池,http连接池。

    1. 使用连接池一定要确保复用,线程池又是如此, 创建连接池的时候很可能一次性创建了多个连接,大多数连接池考虑到性能,会在初始化的时候维护一定数量的最小连接(毕竟初始化连接池的过程一般是一次性的),可以直接使用。如果每次使用连接池都按需创建连接池,那么很可能你只用到一个连接,但是创建了 N 个连接。连接池一般会有一些管理模块,也就是连接池的结构示意图中的绿色部分。举个例子,大多数的连接池都有闲置超时的概念。连接池会检测连接的闲置时间,定期回收闲置的连接,把活跃连接数降到最低(闲置)连接的配置值,减轻服务端的压力。一般情况下,闲置连接由独立线程管理,启动了空闲检测的连接池相当于还会启动一个线程。此外,有些连接池还需要独立线程负责连接保活等功能。因此,启动一个连接池相当于启动了 N 个线程。
  2. http调用,超时,重试,并发

  3. 连接超时参数 ConnectTimeout,让用户配置建连阶段的最长等待时间; 常见的误区:

    1. 连接超时时间不易设置太长时间 2.排查连接超时问题,没有理清问题在哪
  4. 读取超时参数 ReadTimeout,用来控制从 Socket 上读取数据的最长等待时间。 常见误区: 1. 认为出现了读取超时,服务端的执行就会中断。 2.认为读取超时只是 Socket 网络层面的概念,是数据传输的最长耗时,故将其配置得非常短,比如 100 毫秒。 3.认为超时时间越长任务接口成功率就越高,将读取超时参数配置得太长。

  5. 索引不是万能药 虽然数据保存在磁盘中,但其处理是在内存中进行的。为了减少磁盘随机读取次数,InnoDB 采用页而不是行的粒度来保存数据,即数据被分成若干页,以页为单位保存在磁盘中。InnoDB 的页大小,一般是 16KB。各个数据页组成一个双向链表,每个数据页中的记录按照主键顺序组成单向链表;每一个数据页中有一个页目录,方便按照主键查询记录。 B+ 树的特点包括: 最底层的节点叫作叶子节点,用来存放数据; 其他上层节点叫作非叶子节点,仅用来存放目录项,作为索引; 非叶子节点分为不同层次,通过分层来降低每一层的搜索量; 所有节点按照索引键大小排序,构成一个双向链表,加速范围查找 聚簇索引和二级索引 为了实现非主键字段的快速搜索,就引出了二级索引,也叫作非聚簇索引、辅助索引。二级索引,也是利用的 B+ 树的数据结构,二级索引的叶子节点中保存的不是实际数据,而是主键,获得主键值后去聚簇索引中获得数据行。这个过程就叫作回表。

  6. 判等问题 == 和 equals

    1. 比较值的内容,除了基本类型只能使用 == 外,其他类型都需要使用 equals。
    2. Java 的字符串常量池机制。首先要明确的是其设计初衷是节省内存。 当代码中出现双引号形式创建字符串对象时,JVM 会先对这个字符串进行检查,如果字符串常量池中存在相同内容的字符串对象的引用,则将这个引用返回; 否则,创建新的字符串对象,然后将这个引用放入字符串常量池,并返回该引用。这种机制,就是字符串驻留或池化。
    3. 没事别轻易用 intern,如果要用一定要注意控制驻留的字符串的数量,并留意常量表的各项指标
    4. equals 和 == 的区别。业务代码中进行内容的比较,针对基本类型只能使用 ==, 针对 Integer、String 在内的引用类型,需要使用 equals。 Integer 和 String 的坑在于,使用 == 判等有时也能获得正确结果。Integer 127 128 缓存 [-128,127)

10.hashCode 和 equals 要配对实现

    1. Lombok 的 @Data 注解会帮我们实现 equals 和 hashcode 方法,但是有继承关系时,Lombok 自动生成的方法可能就不是我们期望的了
  • 2.@EqualsAndHashCode 默认实现没有使用父类属性。@EqualsAndHashCode(callSuper = true)
  1. 数值计算
  • 1. 使用 BigDecimal 表示和计算浮点数,且务必使用字符串的构造方法来初始化 BigDecimal
    
  • 2.浮点数的字符串格式化也要通过 BigDecimal 进行。对浮点数做精确计算,参与计算的各种数值应该始终使用 BigDecimal,所有的计算都要通过 BigDecimal 的方法进行,切勿只是让 BigDecimal 来走过场。任何一个环节出现精度损失,最后的计算结果可能都会出现误差。
    
  1. Arrays.asList 把数据转换为 List

  2. 不能直接使用 Arrays.asList 来转换基本类型数组
    使用:Arrays.stream 方法来转换,否则可以把 int 数组声明为包装类型 Integer 数组 2.Arrays.asList 返回的 List 不支持增删操作

  3. 对原始数组的修改会影响到我们获得的那个 List

  4. 使用 List.subList 进行切片操作居然会导致 OOM? 业务开发时常常要对 List 做切片处理,即取出其中部分元素构成一个新的 List,我们通常会想到使用 List.subList 方法。但,和 Arrays.asList 的问题类似,List.subList 返回的子 List 不是一个普通的 ArrayList。这个子 List 可以认为是原始 List 的视图,会和原始 List 相互影响。如果不注意,很可能会因此产生 OOM 问题 subList 方法返回的 List 强引用。

14.合适的数据结构干合适的事 1. 使用数据结构考虑平衡时间和空间 要对大 List 进行单值搜索的话,可以考虑使用 HashMap,其中 Key 是要搜索的值,Value 是原始对象,会比使用 ArrayList 有非常明显的性能优势。 这里我们看到的是平衡的艺术,空间换时间,还是时间换空间,只考虑任何一个方面都是不对的。

  1. NullPointerException 场景

    1. 参数值是 Integer 等包装类型,使用时因为自动拆箱出现了空指针异常;
    2. 字符串比较出现空指针异常;
    3. 诸如 ConcurrentHashMap 这样的容器不支持 Key 和 Value 为 null,强行 put null 的 Key 或 Value 会出现空指针异常;
    4. A 对象包含了 B,在通过 A 对象的字段获得 B 之后,没有对字段判空就级联调用 B 的方法出现空指针异常;
    5. 方法或远程服务返回的 List 不是空而是 null,没有进行判空就直接调用 List 的方法出现空指针异常。
  2. 小心MySQL 有关Null的三个坑

    1. 通过 sum 函数统计一个只有 NULL 值的列的总和,比如 SUM(score);
    2. select 记录数量,count 使用一个允许 NULL 的字段,比如 COUNT(score);
    3. 使用 =NULL 条件查询字段值为 NULL 的记录,比如 score=null 条件。
  3. 在MySQL的使用中,对于索引列,建议都设置为not null,因为如果有null的话,MySQL需要单独专门处理null值,会额外耗费性能。

  4. 捕获和处理异常容易犯的错 第一个错 不在业务代码层面考虑异常处理,仅在框架层面粗犷捕获和处理异常。 第二个错,捕获了异常后直接生吞 第三个错,丢弃异常的原始信息 第四个错,抛出异常时不指定任何消息

处理异常的消息: 转换,转换成一种新的一长排除 重试 不能盲目重试 恢复 使用默认的值代替

  1. 太多份对象导致OOM 解释问题原因:大对象的引用 复用等问题 100M 的数据加载到程序内存中,变为 Java 的数据结构就已经占用了 200M 堆内存;这些数据经过 JDBC、MyBatis 等框架其实是加载了 2 份,然后领域模型、DTO 再进行转换可能又加载了 2 次;最终,占用的内存达到了 200M*4=800M。 WeakHashMap缓存容器: WeakHashMap 的特点是 Key 在哈希表内部是弱引用的,当没有强引用指向这个 Key 之后,Entry 会被 GC,即使我们无限往 WeakHashMap 加入数据, 只要 Key 不再使用,也就不会 OOM。 Java引用类型和垃圾回收的关系: 1.垃圾回收器不会回收有强引用的对象; 2.在内存充足时,垃圾回收器不会回收具有软引用的对象; 3.垃圾回收器只要扫描到了具有弱引用的对象就会回收; WeakHashMap 就是利用了这个特点。
    应用场景WeakHashMap :参考hutool工具类使用SimpleCache.class

  2. 弱引用、软引用 1.强引用:最常见的一种,只要该引用存在,就不会被GC。 2.软引用:内存空间不足时,进行回收。 3.弱引用:当JVM进行GC时,则进行回收,无论内存是否充足。 4.虚引用:........。 设计方案缓存是,可以优先选择软引用。因为弱引用,被回收的频率更高。缓存,如果经常被回收的话,就达不到最大利用率。

ConcurrentReferenceHashMap

  1. 循环依赖 直观解决方法时通过set方法去处理,背后的原理其实是缓存。 主要解决方式:使用三级缓存 singletonObjects: 一级缓存, Cache of singleton objects: bean name --> bean instance earlySingletonObjects: 二级缓存, Cache of early singleton objects: bean name --> bean instance 提前曝光的BEAN缓存 singletonFactories: 三级缓存, Cache of singleton factories: bean name --> ObjectFactory

文章内容来自:极客时间 Java 业务开发常见错误 100 例课程,学习时记录整理