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