近期code review几处小问题集锦
1 线程池使用不当
我们的调度系统需要将一堆会员分配给相应的人员来处理,流程如以下伪代码所示:
public void dispatch() {
while (true) {
List<Member> memberList = getUnassignedMemberList(); //获取所有未分配的会员
for(Member each : memberList) {
singleDispatch(each); //为每一个会员分配相应的人员处理
}
try {
Thread.sleep(1000); //休眠1秒后继续分配
} catch (InterruptedException e) {
}
}
}
为了提高分配的速度,我们打算采用多线程的分配方式。一开始使用的是newCachedThreadPool。
private static ExecutorService executor = Executors.newCachedThreadPool();
public void dispatch() {
while (true) {
List<Member> memberList = getUnassignedMemberList(); //获取所有未分配的会员
for(final Member each : memberList) {
executor.submit(new Runnable() {
@Override
public void run() {
singleDispatch(each); //为每一个会员分配相应的人员
}
});
}
try {
Thread.sleep(1000); //休眠1秒后继续分配
} catch (InterruptedException e) {
}
}
}
在压测时发现,load飙升得很高,通过抓栈发现开启了很多线程。原因是:newCachedThreadPool最大线程数为整型的最大值,每提交一个任务,如果没有线程处理,那就产生一个新的线程。当我们for循环提交任务时,开辟了上百个线程,应用程序马上崩溃。
既然发现了原因,我们马上将调整线程池为newFixedThreadPool,这里我们可以设置最大线程数为4,队列长度为整型的最大值。
private static ExecutorService executor = Executors.newFixedThreadPool(4);
但是压测又发现新问题,线程池里的队列长度不断增长,而且分配不断有异常抛出(异常信息为会员已经被分配过)。
原因是:当未分配会员较多时,可能需要5秒才能分配完,然而executor.submit是异步操作,当休眠1秒钟后,马上又进入下一个循环,队列里又将插入重复的会员,这会导致队列长度不断增长,此外,会导致1个会员被分配后,又继续被分配,导致异常产生。
解决方法:使用invokeAll这一同步语句,意思是只有当提交任务都被执行完后,才执行后续语句。
public void dispatch() {
while (true) {
List<Callable<String>> tasks = new ArrayList<Callable<String>>();
List<Member> memberList = getUnassignedMemberList(); //获取所有未分配的会员
for (final Member each : memberList) {
tasks.add(new Callable<String>() {
@Override
public String call() {
singleDispatch(each);
return "ok";
}
});
}
try {
executor.invokeAll(tasks, 480, TimeUnit.SECONDS); //如果8分钟还未执行完,则超时重新再来(鲁棒性保证)
}catch (Exception e) {
}
try {
Thread.sleep(1000); //休眠1秒后继续分配
} catch (InterruptedException e) {
}
}
}
2 NPE(java.lang.NullPointerException)
2.1 情形1
if(case.getType() == Case.TYPE_SELF) {
...
}
这段代码抛出NPE时,直觉认为case为null导致的,后来打日志发现case并不为null,而case.getType()返回值类型为Integer,为null。
最后发现Case.TYPE_SELF返回的是int类型,而case.getType()是个null,null与int两者一比较就报NPE。
这个问题的诡异之处我们直觉上认为Case.TYPE_SELF是个Integer,所以导致排查问题花费了些时间,因此一个建议这种常量如Case.TYPE_SELF都改为Integer,然后对象间比较的时候使用equals,并增加null判断,就可以避免出现问题。
if(null != case.getType() && case.getType().equals(Case.TYPE_SELF))
2.2 情形2
Integer leftNum = (null != leftNumMap && !leftNumMap.isEmpty()) ? leftNumMap.get(stat.getDepartmentId()) : 0;
这个也抛NPE,我们排查了半个多小时,百思不得其解,后来查看class文件发现了原因。
假设我们代码是:
Map<String, Integer> leftNumMap = new HashMap<String , Integer>();
Integer leftNum = (null != leftNumMap && !leftNumMap.isEmpty()) ? leftNumMap.get("test") : 0;
反编译后的代码如下:
HashMap leftNumMap = new HashMap();
Integer leftNum = Integer.valueOf(null != leftNumMap && !leftNumMap.isEmpty()?((Integer)leftNumMap.get("test")).intValue():0);
为什么会这样呢?《你真的会用 Java 中的三目运算符吗?》一文做了说明。三目运算符的语法规范是这样写的:If one of the second and third operands is of primitive type T, and the type of the other is the result of applying boxing conversion (§5.1.7) to T, then the type of the conditional expression is T.
简单的来说就是:当第二,第三位操作数一个为基本类型一个为对象时,其中的对象就会拆箱为基本类型进行操作。
所以,结果就是:由于使用了三目运算符,并且第二、第三位操作数一个是基本类型一个是对象。所以对对象进行拆箱操作,由于该对象为null,所以在拆箱过程中调用null.intValue()的时候就报了NPE。
解决方法很简单:1)要么不用三目运算符,直接使用if else,简单可靠;2)或者将三目运算符操作数都改为对象。
Integer leftNum = (null != leftNumMap && !leftNumMap.isEmpty()) ? leftNumMap.get(stat.getDepartmentId()) : Integer.valueOf(0);
3 Map<K, List<V>>用错
我们代码中经常出现以下这种数据结构,比如key是类目,value是个List,list存储着这个类目下各个子类目的统计数据等。
Map<String,List<MyClass>> myClassListMap = new HashMap<String,List<MyClass>>()
我们插入该数据结构的时候往往得这样写:
void putMyObject(String key, Object value) {
List<Object> myClassList = myClassListMap.get(key);
if(myClassList == null) {
myClassList = new ArrayList<object>();
myClassListMap.put(key,myClassList);
}
myClassList.add(value);
}
当我们希望检查List中的对象是否存在,或删除一个对象,那要遍历整个数据结构,需要更多的代码。
这些代码不仅给可读性带来障碍,更提高了大家出错的概率,比如某一次我们在复杂的业务逻辑中实现putMyObject时,漏写了句:
myClassListMap.put(key,myClassList);
结果导致花费了大量时间才发现这个问题。
当然,细致的测试以及code review能避免出现类似问题,但是有没有一种方法既能从技术上来帮助我们避免此类问题,又能节约代码提高代码可读性?
推荐大家使用google guava包,里面提供了MultiMap数据结构,使用方式如下,非常简洁明了,也不会有机会让我们出错。
Multimap<String, String> myMultimap = ArrayListMultimap.create();
myMultimap.put("女装", "内衣");
myMultimap.put("女装", "羽绒服");
myMultimap.put("女装", "风衣");
myMultimap.put("男装", "皮夹克");
// 获取key "女装"对应的list
Collection<String> womenDressList = myMultimap.get("女装");
// 删除key "女装"对应List中的"羽绒服"
myMultimap.remove("女装", "羽绒服");
近期code review几处小问题集锦的更多相关文章
- code review作业
下面是对结对编程队友12061166 宋天舒的code review 五个优点: 1.代码的风格优秀,注释不多,但是必要的注释还是有的,比如: // 三种模式 // mode1仅统计单个单词 // m ...
- 17款code review工具
本文是码农网原创翻译,转载请看清文末的转载要求,谢谢合作! 好的代码审查器可以大大地帮助程序员提高代码质量,减少错误几率. 虽然现在市场上有许多可用的代码审查工具,但如何挑选也是一个艰巨的任务.在咨询 ...
- Go Code Review Comments 译文(截止2018年7月27日)
持续更新中- 原文最新链接 https://github.com/golang/go/wiki/CodeReviewComments/5a40ba36d388ff1b8b2dd4c1c3fe820b8 ...
- 漫谈Code Review的错误实践
从刚开始工作时到现在,已经写了7年的代码,大部分代码都被人review过,自己也review了很多人的代码.在上一家公司的时候,我负责的一轮面试是专门进行Code Review的练习和经验谈. 通过在 ...
- 【转载】 漫谈Code Review的错误实践
原文地址: https://www.cnblogs.com/chaosyang/p/code-review-wrong-practices.html ------------------------- ...
- Code Review 常见的5个错误模式
原作者:Trisha Gee Code Review 的时候,每个人都会关心最佳实践,但最坏的实践有时可能会更有启示意义. Code Review是研发团队必不可少的,但并不总是正确的.这篇文章指出了 ...
- 我们是怎么做Code Review的
前几天看了<Code Review 程序员的寄望与哀伤>,想到我们团队开展Code Review也有2年了,结果还算比较满意,有些经验应该可以和大家一起分享.探讨.我们为什么要推行Code ...
- Code Review 程序员的寄望与哀伤
一个程序员,他写完了代码,在测试环境通过了测试,然后他把它发布到了线上生产环境,但很快就发现在生产环境上出了问题,有潜在的 bug. 事后分析,是生产环境的一些微妙差异,使得这种 bug 场景在线下测 ...
- Git和Code Review流程
Code Review流程1.根据开发任务,建立git分支, 分支名称模式为feature/任务名,比如关于API相关的一项任务,建立分支feature/api.git checkout -b fea ...
随机推荐
- RedHat下安装OPENCV
1.解压 unzip opencv-2.4.9.zip 2.进入目录,cmake CMakeLists.txt 生成build文件 3.使用命令 make 编译 4.使用命令 make instal ...
- stm32串口之存储与解析
最近在做一个小项目,需要用stm32串口接受Arduino发送的一个不定长的数据,并且解析数据,执行其中的命令:秉着不在中断中做过多任务的思想,我们将从串口中接受到的字符放到一个数组当中. 定义数组 ...
- C#的 构造函数 和 方法重载
构造函数(一本正经的讲构造函数 如果想看不正经的往下翻看方法重载) 方法名称与类名相同,没有返回值类型,连void都没有 用作给类的对象初始化 一个类中可以有多个构造 如果手动添加一个构造,系统不会自 ...
- 笔记本Linux推荐
1.CUB LINUX Cub Linux 是一个最好的选择.他的前身来自著名的 Chromium OS , Cub Linux 能够运行在各种各样的笔记本上面.即便是早年的老机,亦或是现在的新机.从 ...
- PHP--目录处理
__file___ dirname(): dirname()与__file__的组合:dirname(__file__)
- String与StringBuffer的区别
首先,String和StringBuffer主要有2个区别: (1)String类对象为不可变对象,一旦你修改了String对象的值,隐性重新创建了一个新的对象,释放原String对象,StringB ...
- AngularJS学习---REST和自定义服务(REST and Custom Services) ngResource step 11
1.切换目录 git checkout step- npm start 2.效果图 效果图和step 10的没有什么差别,这里主要的改动都是代码,代码做了很多优化,这里效果图就不再贴出来了. 3.实现 ...
- ios8以后,使用UIAlertViw时pop/push页面后,键盘闪一下的问题
代码为 UIAlertView *alert = [[UIAlertView alloc]initWithTitle:@"" message:@"感谢你对我们提出的意见或 ...
- linux学习之——学习路线(摘抄)
摘抄某笔者的Linux练习的道路图(rolistingmap): 对比一下为什么要学习linux 了解Linux的基础常识,这些包括了用户管理.群组的概念.权限的观念等 掌握至多50个以上的常用命令 ...
- VB6.0和VB.Net的函数等对照表
VB6.0和VB.Net的对照表 VB6.0 VB.NET AddItem Object名.AddItem Object名.Items.Add ListBox1.Items.Add ComboBox1 ...