Java高手笔记之业务开发常见错误100例

Java高手笔记之业务开发常见错误100例

在Web环境中使用ThreadLocal出现数据错乱的坑

原因:线程可能重用,导致ThreadLocal中的数据会串

解决︰用完及时清空数据,比如可以自定义HandlerInterceptorAdapter,在preHandle 的时候去设置ThreadLocal,在 afterCompletion时去remove

使用了ConcurrentHashMap 但还是出现了线程安全问题

原因:ConcurrentHashMap只能保证提供的原子性读写操作(比如putlfAbsent、computelfAbsent、replace、compute)是线程安全的

解决︰如果需要确保多个原子性操作整体线程安全,需要自己加锁解决

补充:诸如size、isEmpty和containsValue 等聚合方法,在并发情况下可能会反映ConcurrentHashMap 的中间状态。因此在并发情况下,这些方法的返回值只能用作参考,而不能用于流程控制

使用了ConcurrentHashMap 但却没有发挥性能优势

原因:仍然像HashMap那样使用加锁的方式,来使用ConcurrentHashMap

解决︰考虑使用computelfAbsent、putlfAbsent、getOrDefault等API来提升性能

在不合适的场景下使用CopyOnWriteArrayList导致的性能问题

原因::CopyOnWriteArrayList每次修改复制一份数据

解决︰读多写少的场景才考虑CopyOnWriteArrayList,写多的场景考虑ArrayList

代码加锁

没有理清楚线程安全问题的所在点,导致锁无效

原因1:没有识别线程安全问题的原因胡乱加锁

原因2∶锁是实例级别的,资源是类级别的,无法有效保护

解决∶明确锁和要保护的资源的关系和范围

加锁没有考虑锁的粒度,可能导致性能问题

原因:加锁粒度太大,使得大段代码整体串行执行,出现性能问题

解决︰尽可能降低锁的粒度,仅对必要的代码块甚至是需要保护的资源本身加锁

加锁没有考虑锁的场景,可能导致性能问题

原因:千篇一律使用写锁,可以根据场景更细化地使用高级锁

解决1:考虑使用StampedLock的乐观读的特性,进—步提高性能

解决2:对于读写比例差异明显的场景,使用ReentrantReadWriteLock 细化区分读写锁

解决3:在没有明确需求的情况下,不要轻易开启公平锁特性

多把锁时,要格外小心死锁问题(VisualVM)

原因:多把锁相互等待对方释放,导致死锁

解决:加锁的时候考虑顺序,按顺序加锁不易死锁

工具:使用VisualVM的线程Dump,查看死锁问题并分析死锁原因

线程池

使用Executors声明线程池导致两种类型的OOM

原因1: newFixedThreadPool使用无界队列,队列堆积太多数据导致OOM

原因2: newCachedThreadPool不限制最大线程数并且使用没有任何容量的SynchronousQueue 作为队列,容易开启太多线程导致OOM

解决:手动new ThreadPoolExecutor,根据需求设置合适的核心线程数、最大线程数、线程回策略、队列、拒绝策略,并对线程进行明确的命名以方便排查问题

线程池线程管理策略详解︰如何实现一个更激进的线程池?

原因:Java的线程池倾向于优先使用队列,队列满了再开启更多线程

解决:重写队列的 offer方法直接返回false,数据不入队列,并且自定义RejectedExecutionHandler,触发拒绝策略的时候再把任务加入队列;参考Tomcat的 ThreadPoolExecutor和TaskQueue类

没有复用线程池,导致频繁创建线程的事故

原因:获取线程池的方法每次都返回一个newCachedThreadPool,好在newCachedThreadPool可以闲置回收

解决︰使用静态字段定义线程池,线程池务必重用

混用线程池,导致性能问题

原因:IO绑定操作和CPU绑定操作混用一个线程池,前者因为负担重,线程长期处于忙的状态,导致CPU操作吞吐受到影响

解决∶根据任务的类型声明合适的线程池,不同类型的任务考虑使用独立线程池

扩展:Java 8的 ParallelStream背后是一个公共线程池,别把IO任务使用ParallelStream来处理

CallerRunsPolicy拒绝策略可能带来的问题

原因:如果设置CallerRunsPolicy,那么被拒绝的任务会由提交任务的线程运行,可能会在线程池满载的情况下直接拖垮整个应用

解决:对于Web和Netty场景,要仔细考虑把任务提交到线程池异步执行使用的拒绝策略,除非有明确的需求,否则不考虑使用CallerRunsPolicy拒绝策略

连接池

你知道常见的Client SDK的API,有哪3种形式吗?

形式1:

内部带有连接池的API: SDK内部会先自动通过连接池获取连接(几乎所有的数据库连接池,都是这一类)

形式2:

连接池和连接分离的API:使用者先通过连接池获取连接,再使用连接执行操作(Jedis)

形式3:

非连接池的API:非线程安全,需要使用者自己封装连接池

在多线程环境下使用Jedis出现的线程安全问题

原因:Jedis是连接池和连接分离的API,Jedis类代表连接,不能多线程环境下使用

解决︰每次使用JedisPool先获取到一个Jedis,然后再调用Jedis 的 API,通过addShutdownHook 来关闭JedisPool

不复用Apache CloseableHttpClient会导致什么问题?(jstack、lsof、Wireshark)

连接池如果不复用,代价可能会比每次创建单个连接还要大:

1、连接池可能每次都会创建一定数量的初始连接

2、连接池可能会有一些管理模块,需要创建单独的线程来管理

工具:

1、使用jstack观察到没有复用连接池,会出现大量的Connection evictor线程

2、使用Isof观察到没有复用连接池,会出现大量TCP连接

3、如果复用CloseableHttpClient,使用wireshark 观察HTTP请求重用一个TCP连接的过程

小心数据库连接池打满后出现的性能问题(JConsole)

原因︰数据库连接池最大连接数设置得太小,很可能会因为获取连接的等待时间太长,导致吞吐量低下甚至超时无法获取连接

解决:对类似数据库连接池的重要资源进行持续检测,并设置一半的使用量作为报警阈值,出现预警后及时扩容

工具:使用JConsole 来观察Hikari连接池的MBean,监控活跃连接、等待线程等数据

扩展:修改配置参数务必验证是否生效,并且在监控系统中确认参数是否生效、是否合理,避免明明使用的是Hikari连接池,却还在调整Druid连接池的参数的情况

HTTP调用

连接超时和读取超时的5个认知误区

误区1:连接超时配置得特别长

分析:连接超时很可能是因为防火墙等原因彻底连接不上,不太会出现连接特别慢的现象,不用设置太长

误区2:排查连接超时问题,却没理清连的是哪里

分析:很可能客户端连接的是Nginx,不是实际的后端服务,从后端服务层面排查问题没用

误区3:认为出现了读取超时,服务端的执行就会中断

分析:客户端读取超时,服务端业务逻辑还会持续运行,不能随意假设服务端处理是失败的

误区4:认为读取超时只是Socket网络层面的概念,是数据传输的最长耗时,故将其配置得非常短

分析:读取超时并不是数据在网络传输的时间,需要包含服务端业务逻辑执行的时间

误区5:认为超时时间越长任务接口成功率就越高,将读取超时参数配置得太长

分析:读取超时设置太长,可能导致上游服务被下游拖垮,应该根据SLA设置合适的读取超时。有些时候,快速失败或熔断不是一件坏事儿

Spring Cloud Feign和 Ribbon配合使用,设置超时的三个坑

坑1

原因:默认情况下Feign的读取超时是1秒,这个时间过于短了

解决︰根据自己的需要设置长一点

坑2

原因:如果要配置Feign的读取超时,就必须同时配置连接超时,才能生效

解决:同时配置readTimeout和connectTimeout

坑3

原因:Ribbon配置连接超时和读取超时的参数大小写和Feign略微不同

解决:Ribbon 使用ReadTimeout和 ConnectTimeout,注意大小写

Spring Cloud Ribbon居然会自动重试我的接?

原因:Ribbon对于HTTP Get请求在一个服务器调用失败后,会自动到下一个节点重试一次

解决︰修改参数,设置ribbon.MaxAutoRetriesNextServer=O;并且对于Get请求需要确保接幂等

小心Apache HttpClient对并发连接数的限制

原因:HttpClient对连接数有限制,默认一个域名2个并发,所有域名20个并发

解决:通过HttpClients.custom( ).setMaxConnPerRoute(10).setMaxConnTotal(20)来设置合适的值

数据库事务

两种错误写法导致Spring声明式事务未生效的两个坑

原因:因为private方法无法代理,所以为private方法设置@Transactional注解无法生效事务

解决:除非使用AspectJ做静态织入,否则需要确保只有public方法才设置@Transactional注解

原因:因为通过this自调用方法不走Spring的代理类,所以无法生效事务

解决︰确保事务性方法从外部通过代理类调用。如果一定要从内部调用,就要重新注入当前类调用

事务即便生效也不一定能回滚的两个情况

情况1

原因:只有异常传播出了标记了@Transactional注解的方法,事务才能回滚

解决︰避免 catch住异常,或者通过TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()手动回滚

情况2

原因︰默认情况下,出现 RuntimeException(非受检异常)或Error的时候,Spring 才会回滚事务

解决︰设置@Transactional(rollbackFor = Exception.class),来突破默认不回滚受检异常的限制

不出异常,事务居然也不会提交?

原因:默认事务传播策略是REQUIRED,子方法会复用当前事务,子方法出异常后回滚当前事务,导致父方法也无法提交事务

解决︰设置REQUIRES_NEW方式的事务传播策略,让子方法运行在独立事务中

数据库索引

聚簇索引的3个要点

要点1:B+树,既是索引也是数据

要点2:自动创建,只能有一个

要点3:可以加速主键搜索和查询

考虑二级索引的维护、空间和回表代价

原因

1、维护:需要独立维护—棵B+树,用来加速数据查询和排序;考虑数据页的合并和分裂

2、空间:额外的存储空间

3、回表:二级索引不保存完整数据,只保存索引键和主键字段

解决方案

1、按需创建

2、考虑联合索引

3、针对轻量级字段创建

建了索引但是用不上的3种情况

情况1:查询数据内容不走左匹配

情况2:查询字段使用函数运算

情况3:查询没有使用联合索引最左边的列

多个独立索引和联合索引如何选择的问题

考虑:多个字段会在一个条件中查询,并且更有可能走索引覆盖

不考虑:永远只是单—列的查询

相同的SQL语句,有的时候能走索引有的时候不走索引的现象(MySQL optimizer trace)

原因:MySQL根据不同查询方案的成本来决定是否走索引,索引过滤效果不好的时候,可能走全表扫描更划算

解决︰使用EXPLAIN来观察查询是否会走索引,开启optimizertrace来了解具体原因和成本明细

判等问题

什么时候不能使用==进行值判等(-XX:+PrintStringTableStatistic)

基本类型之间

只能通过==判等

引用类型之间

结论:不能通过==判等,需要使用equals方法

原因:==是判断指针相等、判断对象实例是否是同一个,不代表对象内容是否相同

为什么有的时候String使用==判等会奏效?

原因:直接使用双引号声明出来的两个String 对象指向常量池中的相同字符串

工具:使用-XX:+PrintStringTableStatistic查看字符串常量表

包装类型之间

Integer有的时候使用= =判等会奏效?

原因:Integer内部其实做了缓存,默认缓存[-128,127],在这个区间==奏效

实现equals方法可能出现的诸多坑

原因︰考虑性能先进行指针判等、需要对另一方进行判空、确保类型相同的情况下再进行类型强制转换

解决:使用IDE帮我们生成代码

equals和 hashCode没有配对实现的坑

原因:对象存入哈希表的行为不可测

解决∶务必配对实现,使用IDE帮我们生成代码

equals和compareTo实现逻辑不一致的坑

原因:ArrayList.indexOf使用equals判断对象是否相等,而Collections.binarySearch使用compareTo方法来比较对象以实现搜索

解决:对于自定义的类型,如果要实现Comparable,记得equals、hashCode、compareTo三者逻辑一致

Lombok @EqualsAndHashCode可能的两个坑

坑1

原因:Lombok的@EqualsAndHashCode 注解实现equals和hashCode的时候,默认使用类的所有非static、非 transient的字段

解决:使用@EqualsAndHashCode.Exclude 排除一些字段

坑2

原因:Lombok的@EqualsAndHashCode注解实现equals和hashCode的时候,默认不考虑父类

解决︰设置callSuper = true

数值计算

使用double进行浮点数运算的坑

原因:浮点数无法精确存储,计算会损失精度

解决:

1、使用 BigDecimal表示和计算浮点数,且务必使用字符串的构造方法来初始化 BigDecimal

2、如果一定要用 Double来初始化 BigDecimal的话,可以使用BigDecimal valueOf方法

对double 或 float进行舍入格式化的坑

原因:使用 double或foat配合 String. format或DecimalFormat进行格式化舍入,也会有精度问题

解决:还是需要使用 BigDecima,配合 setscale进行舍入

使用equals对 BigDecimal进行判等的坑

原因:使用equals 比较BigDecimal 会同时比较value 和scale,1.0O不等于1,和我们想的不一样

解决︰如果只比较BigDecimal 的value,可以使用compareTo方法

把 BigDecimal 作为 Key加入HashSet的坑

原因:BigDecimal 的equals和 hashCode方法会同时考虑value 和scale,BigDecimal 的值1.0和1,虽然value相同但是scale 不同

解决︰使用TreeSet替换HashSet,或者先使用stripTrailingZeros方法去掉尾部的零

小心数值溢出但是没有任何异常的坑

原因:大数值计算,数值可能默默溢出,没有任何异常解决:

1、使用Math类的addExact、subtractExact 等 xxExact方法进行数值运算,在溢出时能抛出异常

2、使用大数类 BigInteger,需要转到Long 时使用longValueExact,在溢出时能抛出异常

集合类

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

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

原因:只能是把 int装箱为Integer,不可能把int数组装箱为Integer 数组,int数组整体作为了一个对象成为了泛型类型T解决:使用Arrays.stream 或传入Integer[]

坑2: Arrays.asList返回的List不支持增删操作

原因:Arrays.asList返回的List并不是我们期望的java.util.ArrayList,而是Arrays 的内部类ArrayList

解决:重新new 一个ArrayList 初始化Arrays.asList返回的List

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

原因︰内部 ArrayList其实是直接使用了原始的数组

解决:重新new一个ArrayList初始化Arrays.asList返回的List

使用List.subList进行切片操作居然会导致OOM

原因:List.subList返回的是内部类SubList 会引用原始List,SubList有强引用时会导致原来的 List也无法GC

解决∶

1、不直接使用subList方法返回的SubList,而是使用newArrayList来重新构建一个普通的ArrayList

2、对于Java 8使用Stream的skip和limit API来跳过流中的元素,以及限制流中元素的个数

使用数据结构需要考虑平衡时间和空间(MAT)

时间:要实现快速查询元素,可以考虑使用HashMap替换ArrayList

空间:但是HashMap 数据结构存储利用率相比ArrayList会低很多

工具:使用MAT观察数据结构内存占用

LinkedList适合什么场景呢?

1、在各种常用场景下,LinkedList几乎都不能在性能上胜出ArrayList

2、使用任何数据结构最好根据自己的使用场景来评估和测试,以数据说话

空值处理

注意5种可能出现空指针的情况 (Arthas)

情况1

原因:参数值是Integer 等包装类型,使用时因为自动拆箱出现了空指针异常

解决:对于Integer 的判空,可以使用Optional.ofNullable来构造一个Optional,然后使用orElse(O)把 null替换为默认值再进行操作

情况2

原因:字符串比较出现空指针异常

解决:对于String和字面量的比较,可以把字面量放在前面,比如"OK".equals(s)

情况3

原因:诸如ConcurrentHashMap这样的容器不支持Key和Value 为null,强行put null的Key 或 Value 会出现空指针异常解决∶不要存null

情况4

原因:A对象包含了B,在通过A对象的字段获得B之后,没有对字段判空就级联调用B的方法会出现空指针异常

解决︰使用Optional.ofNullable 配合map和ifPresent 方法

情况5

原因:方法或远程服务返回的List不是空而是null,没有进行判空就直接调用List的方法,出现空指针异常

解决:使用Optional.ofNullable包装一下返回值,然后通过.orElse(Collections.emptyList()),实现在List为 null的时候获得一个空的List

工具

使用Arthas的 watch命令观察方法入参,定位null参数,使用stack命令观察调用栈定位调用路径

POJO字段设置默认值导致数据库中的原始值被覆盖的坑

原因:POJO中的字段有默认值,如果客户端不传值,就会赋值为默认值,导致每次都把默认值更新到了数据库中

解决:POJO字段不要设置默认值,如果怕没值可以让数据库设置默认值

你见过数据库中出现null字符串的问题吗?

原因:字符串格式化时,可能会把null值格式化为null字符串

解决:使用Optional的 orElse方法一键把空转换为空字符串

客户端不传值以及传null在POJO中都是null,如何区分?

原因:对于JSON到DTO的反序列化过程,null的表达是有歧义的,客户端不传某个属性,或者传 null,这个属性在DTO中都是null

解决︰使用Optional来包装,以区分客户端不传数据还是故意传null

MySQL中涉及NULL的3个容易忽略的坑

坑1

原因:MySQL中sum函数没统计到任何记录时,会返回null而不是0

解决:可以使用IFNULL函数把NULL转换为0

坑2

原因:MySQL中count字段不统计NULL值

解决:可以使用IFNULL函数把NULL转换为О

坑3

原因:MySQL中使用诸如=、<、>这样的算数比较操作符比较NULL的结果总是NULL,这种比较就显得没有任何意义

解决∶对NULL进行判断只能使用IS NULL、IS NOT NULL或者ISNULL()函数

异常处理

————————————————

版权声明:本文为CSDN博主「Apple_Web」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/belongtocode/article/details/113684078

你可能感兴趣的:(Java高手笔记之业务开发常见错误100例)