阿里巴巴 Java 开发手册学习总结(二):并发、异常与日志
Published in:2025-06-18 | category: Java

本篇整理阿里巴巴 Java 开发手册中并发编程、异常处理和日志规约部分,这三块是日常开发中最容易出线上 bug 的区域。

一、并发编程

线程池不允许用 Executors 创建

手册明确禁止使用以下创建方式:

1
2
3
// 禁止
ExecutorService pool = Executors.newFixedThreadPool(10);
ExecutorService pool = Executors.newCachedThreadPool();

原因:

  • newFixedThreadPoolnewSingleThreadExecutor 的等待队列是 LinkedBlockingQueue长度 Integer.MAX_VALUE,可能堆积大量请求导致 OOM。
  • newCachedThreadPool 允许创建的线程数是 Integer.MAX_VALUE,极端情况下线程数暴涨,OOM。

正确做法,手动构造 ThreadPoolExecutor

1
2
3
4
5
6
7
8
9
10
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // corePoolSize
10, // maximumPoolSize
60L, TimeUnit.SECONDS, // keepAliveTime
new ArrayBlockingQueue<>(200), // 有界队列,防止无限堆积
new ThreadFactoryBuilder()
.setNameFormat("order-pool-%d")
.build(), // 线程命名,便于排查
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);

线程要有名字

无名线程在日志和线程 dump 里显示为 Thread-0Thread-1,出问题时完全无法定位。

SimpleDateFormat 线程不安全

1
2
3
4
5
6
7
8
9
10
// 错误:SimpleDateFormat 有状态,多线程共享会产生日期错乱
private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd");

// 正确方案一:每次创建新实例
String format(Date date) {
return new SimpleDateFormat("yyyy-MM-dd").format(date);
}

// 正确方案二:Java 8 DateTimeFormatter(不可变,线程安全)
private static final DateTimeFormatter DTF = DateTimeFormatter.ofPattern("yyyy-MM-dd");

加锁顺序要一致,避免死锁

多个线程需要同时获取多把锁时,必须保证所有线程以相同顺序加锁

1
2
3
4
// 两个方法都先锁 lockA 再锁 lockB——不会死锁
synchronized(lockA) {
synchronized(lockB) { ... }
}

如果一个方法 A→B 加锁,另一个方法 B→A 加锁,就会产生死锁。


二、异常处理

不要捕获 Exception 大类

1
2
3
4
5
6
// 错误:吞掉了所有异常,包括你不打算处理的
try {
doSomething();
} catch (Exception e) {
log.error("error", e);
}

这么写的问题:

  • 把业务异常、系统异常、NPE 全都一并吞掉
  • 上层调用者收不到任何信号,无法做针对性处理

正确做法:

1
2
3
4
5
6
7
8
9
10
try {
doSomething();
} catch (BusinessException e) {
// 业务异常,可以正常处理
return Result.fail(e.getCode(), e.getMessage());
} catch (IOException e) {
// 系统异常,需要告警
log.error("IO error", e);
throw new ServiceException("系统异常", e);
}

不要用异常做流程控制

1
2
3
4
5
6
7
8
9
10
11
12
13
// 错误:用异常控制流程,性能差且语义不清
try {
int age = Integer.parseInt(ageStr);
} catch (NumberFormatException e) {
age = 0; // 用异常来处理正常的无效输入
}

// 正确:提前校验
if (StringUtils.isNumeric(ageStr)) {
age = Integer.parseInt(ageStr);
} else {
age = 0;
}

finally 里不要有 return

1
2
3
4
5
6
// 错误:finally 里的 return 会覆盖 try 里的 return,且会吞掉异常
try {
return doWork();
} finally {
return "fallback"; // 这会让 doWork() 抛出的异常消失
}

自定义异常要加 cause

1
2
3
4
5
// 错误:原始堆栈丢失,排查困难
throw new ServiceException("操作失败");

// 正确:保留原始异常
throw new ServiceException("操作失败", e);

三、日志规约

不要用 System.out.println

生产代码中任何 System.out 都不应该存在。原因:

  • 输出到标准输出流,不受日志框架管理(无日志级别、无日志轮转)
  • 频繁调用会阻塞 IO

占位符而不是字符串拼接

1
2
3
4
5
// 错误:无论日志级别是否开启,字符串拼接都会执行
log.debug("User info: " + user.toString());

// 正确:只有在 debug 级别开启时才会格式化字符串
log.debug("User info: {}", user);

打印异常时用 e 参数

1
2
3
4
5
// 错误:只打印了 message,没有堆栈信息
log.error("error: " + e.getMessage());

// 正确:完整堆栈
log.error("Failed to process order {}", orderId, e);

慎用 warn,不要泛滥

手册建议:warn 应该表示”需要人工关注但不影响主流程”的情况。如果 warn 日志太多,oncall 的人会失去敏感性,真正需要关注的信息被淹没。


小结

并发、异常、日志是线上 bug 的高发区,原因都是局部看起来能跑,全局才暴露问题:线程池在低负载下不出事,流量一上来 OOM;异常吞掉了但业务数据已经错了;日志不完整导致问题无法复现。养成规范习惯,比事后排查省力得多。

Prev:
阿里巴巴 Java 开发手册学习总结(三):MySQL 规约与工程规范
Next:
阿里巴巴 Java 开发手册学习总结(一):编程规约