27、资源释放写到 finally
比如在使用一个 api 类锁或者进行 IO 操作的时候,需要主动写代码需释放资源,为了能够保证资源能够被真正释放,那么就需要在 finally 中写代码保证资源释放。
如图所示,就是 CopyOnWriteArrayList 的 add 方法的实现,最终是在 finally 中进行锁的释放。
28、使用线程池代替手动创建线程
使用线程池还有以下好处:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统 的稳定性,使用线程池可以进行统一的分配,调优和监控。
所以为了达到更好的利用资源,提高响应速度,就可以使用线程池的方式来代替手动创建线程。
29、线程设置名称
在日志打印的时候,日志是可以把线程的名字给打印出来。
如上图,日志打印出来的就是 tom 猫的线程。
所以,设置线程的名称可以帮助我们更好的知道代码是通过哪个线程执行的,更容易排查问题。
30、涉及线程间可见性加 volatile
在 RocketMQ 源码中有这么一段代码
在消费者在从服务端拉取消息的时候,会单独开一个线程,执行 while 循环,只要 stopped 状态一直为 false,那么就会一直循环下去,线程就一直会运行下去,拉取消息。
当消费者客户端关闭的时候,就会将 stopped 状态设置为 true,告诉拉取消息的线程需要停止了。但是由于并发编程中存在可见性的问题,所以虽然客户端关闭线程将 stopped 状态设置为 true,但是拉取消息的线程可能看不见,不能及时感知到数据的修改,还是认为 stopped 状态设置为 false,那么就还会运行下去。
针对这种可见性的问题,java 提供了一个 volatile 关键字来保证线程间的可见性。
所以,源码中就加了 volatile 关键字。
加了 volatile 关键字之后,一旦客户端的线程将 stopped 状态设置为 true 时候,拉取消息的线程就能立马知道 stopped 已经是 false 了,那么再次执行 while 条件判断的时候,就不成立,线程就运行结束了,然后退出。
31、考虑线程安全问题
在平时开发中,有时需要考虑并发安全的问题。
举个例子来说,一般在调用第三方接口的时候,可能会有一个鉴权的机制,一般会携带一个请求头 token 参数过去,而 token 也是调用第三方接口返回的,一般这种 token 都会有个过期时间,比如 24 小时。
我们一般会将 token 缓存到 Redis 中,设置一个过期时间。向第三方发送请求时,会直接从缓存中查找,但是当从 Redis 中获取不到 token 的时候,我们都会重新请求 token 接口,获取 token,然后再设置到缓存中。
整个过程看起来是没什么问题,但是实则隐藏线程安全问题。
假设当出现并发的时候,同时来两个线程 AB 从缓存查找,发现没有,那么 AB 此时就会同时调用 token 获取接口。假设 A 先获取到 token,B 后获取到 token,但是由于 CPU 调度问题,线程 B 虽然后获取到 token,但是先往 Redis 存数据,而线程 A 后存,覆盖了 B 请求的 token。
这下就会出现大问题,最新的 token 被覆盖了,那么之后一定时间内 token 都是无效的,接口就请求不通。
针对这种问题,可以使用 double check 机制来优化获取 token 的问题。
所以,在实际中,需要多考虑考虑业务是否有线程安全问题,有集合读写安全问题,那么就用线程安全的集合,业务有安全的问题,那么就可以通过加锁的手段来解决。
32、慎用异步
虽然在使用多线程可以帮助我们提高接口的响应速度,但是也会带来很多问题。
事务问题
一旦使用了异步,就会导致两个线程不是同一个事务的,导致异常之后无法正常回滚数据。
cpu 负载过高
之前有个小伙伴遇到需要同时处理几万调数据的需求,每条数据都需要调用很多次接口,为了达到老板期望的时间要求,使用了多线程跑,开了很多线程,此时会发现系统的 cpu 会飙升
意想不到的异常
还是上面的提到的例子,在测试的时候就发现,由于并发量激增,在请求第三方接口的时候,返回了很多错误信息,导致有的数据没有处理成功。
虽然说慎用异步,但不代表不用,如果可以保证事务的问题,或是 CPU 负载不会高的话,那么还是可以使用的。
33、减小锁的范围
减小锁的范围就是给需要加锁的代码加锁,不需要加锁的代码不要加锁。这样就能减少加锁的时间,从而可以较少锁互斥的时间,提高效率。
![](/i/l/?n=22&i=blog/1251645/202210/1251645-20221030154001696-1243151885.png)
比如 CopyOnWriteArrayList 的 addAll 方法的实现,lock.lock(); 代码完全可以放到代码的第一行,但是作者并没有,因为前面判断的代码不会有线程安全的问题,不放到加锁代码中可以减少锁抢占和占有的时间。
34、有类型区分时定义好枚举
比如在项目中不同的类型的业务可能需要上传各种各样的附件,此时就可以定义好不同的一个附件的枚举,来区分不同业务的附件。
不要在代码中直接写死,不定义枚举,代码阅读起来非常困难,直接看到数字都是懵逼的。。
35、远程接口调用设置超时时间
比如在进行微服务之间进行 rpc 调用的时候,又或者在调用第三方提供的接口的时候,需要设置超时时间,防止因为各种原因,导致线程”卡死“在那。
我以前就遇到过线上就遇到过这种问题。当时的业务是订阅 kafka 的消息,然后向第三方上传数据。在某个周末,突然就接到电话,说数据无法上传了,通过排查线上的服务器才发现所有的线程都线程”卡死“了,最后定位到代码才发现原来是没有设置超时时间。
36、集合使用应当指明初始化大小
比如在写代码的时候,经常会用到 List、Map 来临时存储数据,其中最常用的就是 ArrayList 和 HashMap。但是用不好可能也会导致性能的问题。
比如说,在 ArrayList 中,底层是基于数组来存储的,数组是一旦确定大小是无法再改变容量的。但不断的往 ArrayList 中存储数据的时候,总有那么一刻会导致数组的容量满了,无法再存储其它元素,此时就需要对数组扩容。所谓的扩容就是新创建一个容量是原来 1.5 倍的数组,将原有的数据给拷贝到新的数组上,然后用新的数组替代原来的数组。
在扩容的过程中,由于涉及到数组的拷贝,就会导致性能消耗;同时 HashMap 也会由于扩容的问题,消耗性能。所以在使用这类集合时可以在构造的时候指定集合的容量大小。
37、尽量不要使用 BeanUtils 来拷贝属性
在开发中经常需要对 JavaBean 进行转换,但是又不想一个一个手动 set,比较麻烦,所以一般会使用属性拷贝的一些工具,比如说 Spring 提供的 BeanUtils 来拷贝。不得不说,使用 BeanUtils 来拷贝属性是真的舒服,使用一行代码可以代替几行甚至十几行代码,我也喜欢用。
但是喜欢归喜欢,但是会带来性能问题,因为底层是通过反射来的拷贝属性的,所以尽量不要用 BeanUtils 来拷贝属性。
比如你可以装个 JavaBean 转换的插件,帮你自动生成转换代码;又或者可以使用性能更高的 MapStruct 来进行 JavaBean 转换,MapStruct 底层是通过调用(settter/getter)来实现的,而不是反射来快速执行。
38、使用 StringBuilder 进行字符串拼接
如下代码:
String str1 = "123"; String str2 = "456"; String str3 = "789"; String str4 = str1 + str2 + str3;
使用 + 拼接字符串的时候,会创建一个 StringBuilder,然后将要拼接的字符串追加到 StringBuilder,再 toString,这样如果多次拼接就会执行很多次的创建 StringBuilder,z 执行 toString 的操作。
所以可以手动通过 StringBuilder 拼接,这样只会创建一次 StringBuilder,效率更高。
StringBuilder sb = new StringBuilder(); String str = sb.append("123").append("456").append("789").toString();
39、@Transactional 应指定回滚的异常类型
平时在写代码的时候需要通过 rollbackFor 显示指定需要对什么异常回滚,原因在这:
默认是只能回滚 RuntimeException 和 Error 异常,所以需要手动指定,比如指定成 Expection 等。
40、谨慎方法内部调用动态代理的方法
如下事务代码
@Service public class PersonService { public void update(Person person) { // 处理 updatePerson(person); } @Transactional(rollbackFor = Exception.class) public void updatePerson(Person person) { // 处理 } }
update 调用了加了@Transactional 注解的 updatePerson 方法,那么此时 updatePerson 的事务就是失效。
其实失效的原因不是事务的锅,是由 AOP 机制决定的,因为事务是基于 AOP 实现的。AOP 是基于对象的代理,当内部方法调用时,走的不是动态代理对象的方法,而是原有对象的方法调用,如此就走不到动态代理的代码,就会失效了。
如果实在需要让动态代理生效,可以注入自己的代理对象
@Service public class PersonService { @Autowired private PersonService personService; public void update(Person person) { // 处理 personService.updatePerson(person); } @Transactional(rollbackFor = Exception.class) public void updatePerson(Person person) { // 处理 } }
41、需要什么字段 select 什么字段
查询全字段有以下几点坏处:
增加不必要的字段的网络传输
比如有些文本的字段,存储的数据非常长,但是本次业务使用不到,但是如果查了就会把这个数据返回给客户端,增加了网络传输的负担
会导致无法使用到覆盖索引
比如说,现在有身份证号和姓名做了联合索引,现在只需要根据身份证号查询姓名,如果直接 select name 的话,那么在遍历索引的时候,发现要查询的字段在索引中已经存在,那么此时就会直接从索引中将 name 字段的数据查出来,返回,而不会继续去查找聚簇索引,减少回表的操作。
所以建议是需要使用什么字段查询什么字段。比如 mp 也支持在构建查询条件的时候,查询某个具体的字段。
Wrappers.query().select("name");
42、不循环调用数据库
不要在循环中访问数据库,这样会严重影响数据库性能。
比如需要查询一批人员的信息,人员的信息存在基本信息表和扩展表中,错误的代码如下:
public List<PersonVO> selectPersons(List<Long> personIds) { List<PersonVO> persons = new ArrayList<>(personIds.size()); List<Person> personList = personMapper.selectByIds(personIds); for (Person person : personList) { PersonVO vo = new PersonVO(); PersonExt personExt = personExtMapper.selectById(person.getId()); // 组装数据 persons.add(vo); } return persons; }
遍历每个人员的基本信息,去数据库查找。
正确的方法应该先批量查出来,然后转成 map:
public List<PersonVO> selectPersons(List<Long> personIds) { List<PersonVO> persons = new ArrayList<>(personIds.size()); List<Person> personList = personMapper.selectByIds(personIds); //批量查询,转换成Map List<PersonExt> personExtList = personExtMapper.selectByIds(person.getId()); Map<String, PersonExt> personExtMap = personExtList.stream().collect(Collectors.toMap(PersonExt::getPersonId, Function.identity())); for (Person person : personList) { PersonVO vo = new PersonVO(); //直接从Map中查找 PersonExt personExt = personExtMap.get(person.getId()); // 组装数据 persons.add(vo); } return persons; }
43、用业务代码代替多表 join
如上面代码所示,原本也可以将两张表根据人员的 id 进行关联查询。但是不推荐这么,阿里也禁止多表 join 的操作
而之所以会禁用,是因为 join 的效率比较低。
MySQL 是使用了嵌套循环的方式来实现关联查询的,也就是 for 循环会套 for 循环的意思。用第一张表做外循环,第二张表做内循环,外循环的每一条记录跟内循环中的记录作比较,符合条件的就输出,这种效率肯定低。
44、装上阿里代码检查插件
我们平时写代码由于各种因为,比如什么领导啊,项目经理啊,会一直催进度,导致写代码都来不及思考,怎么快怎么来,cv 大法上线,虽然有心想写好代码,但是手确不听使唤。所以我建议装一个阿里的代码规范插件,如果有代码不规范,会有提醒,这样就可以知道哪些是可以优化的了。
如果你有强迫症,相信我,装了这款插件,你的代码会写的很漂亮。
45、及时跟同事沟通
写代码的时候不能闭门造车,及时跟同事沟通,比如刚进入一个新的项目的,对项目工程不熟悉,一些技术方案不了解,如果上来就直接写代码,很有可能就会踩坑。
标签:调用,技巧,17.45,代码,List,代码优化,person,token,线程 From: https://www.cnblogs.com/midiyu/p/16841438.html