常见 Java 代码缺陷及规避方式(上):https://developer.aliyun.com/article/1480648
▐ 线程安全问题
JVM 的内存模型十分复杂,难以理解, <>告诉我们,除非你对 JVM 的线程安全原理十分熟悉,否则应该严格遵守基本的 Java 线程安全规则,使用 Java 内置的线程安全的类及关键字。
- 熟练使用线程安全类
ConcurrentHashMap
反例:
map.get 以及 map.put 操作是非原子操作,多线程并发修改的情况下可能导致一致性问题。比如线程 A 调用 append 方法,在第 6 行时,线程 B 删除了 key。
public class ConcurrentHashMapExample { private Map<String, String> map = new ConcurrentHashMap<>(); public void appendIfExists(String key, String suffix) { String value = map.get(key); if (value != null) { map.put(key, value + suffix); } } }
正例:
public class ConcurrentHashMapExample { private Map<String, String> map = new ConcurrentHashMap<>(); public void append(String key, String suffix) { // 使用 computeIfPresent 原子操作 map.computeIfPresent(key, (k, v) -> v + suffix); } }
- 保证变更的原子性
反例:
@Getter public class NoAtomicDiamondParser { private volatile int start; private volatile int end; public NoAtomicDiamondParser() { Diamond.addListener("dataId", "groupId", new ManagerListenerAdapter() { @Override public void receiveConfigInfo(String s) { JSONObject jsonObject = JSON.parseObject(s); start = jsonObject.getIntValue("start"); end = jsonObject.getIntValue("end"); } }); } } public class MyController{ private final NoAtomicDiamondParser noAtomicDiamondParser; public void handleRange(){ // end 读取的旧值, start 读取的新值, start 可能大于 end int end = noAtomicDiamondParser.getEnd(); int start = noAtomicDiamondParser.getStart(); } }
正例:
@Getter public class AtomicDiamondParser { private volatile Range range; public AtomicDiamondParser() { Diamond.addListener("dataId", "groupId", new ManagerListenerAdapter() { @Override public void receiveConfigInfo(String s) { range = JSON.parseObject(s, Range.class); } }); } @Data public static class Range { private int start; private int end; } } public class MyController { private final AtomicDiamondParser atomicDiamondParser; public void handleRange() { Range range = atomicDiamondParser.getRange(); System.out.println(range.getStart()); System.out.println(range.getEnd()); } }
- 使用不可变对象
当一个对象是不可变的,那这个对象内就自然不存在线程安全问题,如果需要修改这个对象,那就必须创建一个新的对象,这种方式适用于简单的值对象类型,常见的例子就是 java 中的 String
和 BigDecimal
。对于上面一个例子,我们也可以将 Range
设计为一个通用的值对象。
正例:
@Getter public class AtomicDiamondParser { private volatile Range range; public AtomicDiamondParser() { Diamond.addListener("dataId", "groupId", new ManagerListenerAdapter() { @Override public void receiveConfigInfo(String s) { JSONObject jsonObject = JSON.parseObject(s); int start = jsonObject.getIntValue("start"); int end = jsonObject.getIntValue("end"); range = new Range(start, end); } }); } // lombok 注解会保证 Range 类的不变性 @Value public static class Range { private int start; private int end; } }
- 正确性优先于性能
不要因为担心性能问题而放弃使用 synchronized
,volatile
等关键字,或者采用一些非常规写法
反例 双重检查锁:
class Foo { // 缺少 volatile 关键字 private Helper helper = null; public Helper getHelper() { if (helper == null) synchronized(this) { if (helper == null) helper = new Helper(); } return helper; } }
在上述例子中,在 helper 字段上增加 volatile 关键字,能够在 java 5 及之后的版本中保证线程安全。
正例:
class Foo { private volatile Helper helper = null; public Helper getHelper() { if (helper == null) synchronized(this) { if (helper == null) helper = new Helper(); } return helper; } }
正例3(推荐):
class Foo { private Helper helper = null; public synchronized Helper getHelper() { if (helper == null) helper = new Helper(); } return helper; } class Foo { private Helper helper = null; public synchronized Helper getHelper() { if (helper == null) helper = new Helper(); } return helper; }
并不严谨的 Diamond Parser:
/** * 省略异常处理等其他逻辑 */ @Getter public class DiamondParser { // 缺少 volatile 关键字 private Config config; public DiamondParser() { Diamond.addListener("dataId", "groupId", new ManagerListenerAdapter() { @Override public void receiveConfigInfo(String s) { config = JSON.parseObject(s, Config.class); } }); } @Data public static class Config { private String name; } }
这种 Diamond 写法可能从来没有发生过线上问题,但这种写法也确实是不符合 JVM 线程安全原则。未来某一天你的代码跑在另一个 JVM 实现上,可能就有问题了。
▐ 线程池使用不当
反例 1:
public class ThreadPoolExample { // 没有任何限制的线程池, 使用起来很方便, 但当一波请求高峰到达时, 可能会创建大量线程, 导致系统崩溃 private static Executor executor = Executors.newCachedThreadPool(); }
反例 2:
public class StreamParallelExample { public List<String> batchQuery(List<String> ids){ // 看上去很优雅, 但 ForkJoinPool 的队列是没有大小限制的, 并且线程数量很少, 如果 ids 列表很大可能导致 OOM // parallelStream 更适合计算密集型任务, 不要在任务中做远程调用 return ids.parallelStream() .map(this::queryFromRemote) .collect(Collectors.toList()); } private String queryFromRemote(String id){ // 从远程查询 } }
- 手动创建线程池
正例:
public class ManualCreateThreadPool { // 手动创建资源有限的线程池 private Executor executor = new ThreadPoolExecutor(10, 10, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<>(1000), new ThreadFactoryBuilder().setNameFormat("work-%d").build()); }
▐ 异常处理不当
和 NPE 一样,异常处理也同样是我们每天都需要面对的问题,但很多代码中往往会出现:
反例 1:
重复且繁琐的的异常处理逻辑
@Slf4j public class DuplicatedExceptionHandlerExample { private UserService userService; public User query(String id) { try { return userService.query(id); } catch (Exception e) { log.error("query error, userId: {}", id, e); return null; } } public User create(String id) { try { return userService.create(id); } catch (Exception e) { log.error("query error, userId: {}", id, e); return null; } } }
反例 2:
异常被吞掉或者丢失部分信息
@Slf4j public class ExceptionShouldLogOrThrowExample { private UserService userService; public User query(String id) { try { return userService.query(id); } catch (Exception e) { // 异常被吞并, 问题被隐藏 return null; } } public User create(String id) { try { return userService.create(id); } catch (Exception e) { // 堆栈丢失, 后续难以定位问题 log.error("query error, userId: {}, error: {}", id,e.getMessage() ); return null; } } }
反例 3:
对外抛出未知异常, 导致调用方序列化失败
public class OpenAPIService { public void handle(){ // HSF 服务对外抛出 client 中未定义的异常, 调用方反序列化失败 throw new InternalSystemException(""); } }
- 通过 AOP 统一异常处理
- 避免未知异常抛给调用方, 将未知异常转为 Result 或者通用异常类型
- 统一异常日志的打印和监控
- 处理 Checked Exception
Checked Exception 是在编译期要求必须处理的异常,也就是非 RuntimeException 类型的异常,但 Java Checked 的异常给接口的调用者造成了一定的负担,导致异常声明层层传递,如果顶层能够处理该异常,我们可以通过 lombok 的 @SneakyThrows
注解规避 Checked exception
- Try catch 线程逻辑
反例:
@RequiredArgsConstructor public class ThreadNotTryCatch { private final ExecutorService executorService; public void handle() { executorService.submit(new Runnable() { @Override public void run() { // 未捕获异常, 线程直接退出, 异常信息丢失 remoteInvoke(); } }); } }
正例:
@RequiredArgsConstructor @Slf4j public class ThreadNotTryCatch { private final ExecutorService executorService; public void handle() { executorService.submit(new Runnable() { @Override public void run() { try { remoteInvoke(); } catch (Exception e) { log.error("handle failed", e); } } }); } }
常见 Java 代码缺陷及规避方式(下):https://developer.aliyun.com/article/1480646