常见 Java 代码缺陷及规避方式(中)

简介: 常见 Java 代码缺陷及规避方式(中)

常见 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 中的 StringBigDecimal。对于上面一个例子我们也可以将 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;
    }
}


  • 正确性优先于性能


不要因为担心性能问题而放弃使用 synchronizedvolatile 等关键字,或者采用一些非常规写法
反例 双重检查锁:

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 统一异常处理


  1. 避免未知异常抛给调用方, 将未知异常转为 Result 或者通用异常类型
  2. 统一异常日志的打印和监控


  • 处理 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


目录
相关文章
|
5天前
|
SQL Java 数据库
java代码中调用dao层查询接口,代码没有返回数据,打印出的sql查出了数据
java代码中调用dao层查询接口,代码没有返回数据,打印出的sql查出了数据
10 1
|
5天前
|
安全 Java 开发者
Java编程:深入探索其原理、特性与实战代码
Java编程:深入探索其原理、特性与实战代码
10 1
|
6天前
|
存储 算法 Java
滚雪球学Java(20):Java泛型与枚举:提升代码灵活性与可读性
【4月更文挑战第9天】🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
19 1
滚雪球学Java(20):Java泛型与枚举:提升代码灵活性与可读性
|
9天前
|
Java 测试技术 持续交付
云效产品使用常见问题之通过流水线构建的java代码,在docker文件里拿到失败如何解决
云效作为一款全面覆盖研发全生命周期管理的云端效能平台,致力于帮助企业实现高效协同、敏捷研发和持续交付。本合集收集整理了用户在使用云效过程中遇到的常见问题,问题涉及项目创建与管理、需求规划与迭代、代码托管与版本控制、自动化测试、持续集成与发布等方面。
|
15天前
|
Java 测试技术
滚雪球学Java(12):JavaSE-if条件语句入门指南:掌握代码分支流程!
【4月更文挑战第1天】🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
50 1
滚雪球学Java(12):JavaSE-if条件语句入门指南:掌握代码分支流程!
|
18天前
|
Java
与JShell一起舞动代码的魔法:Java9交互式编程体验
与JShell一起舞动代码的魔法:Java9交互式编程体验
31 0
|
18天前
|
Java
代码的魔法师:Java反射工厂模式详解
代码的魔法师:Java反射工厂模式详解
27 0
|
28天前
|
Java
使用Java代码打印log日志
使用Java代码打印log日志
89 1
|
Java BI API
在Java代码中打日志需要注意什么?
日志是什么?日志是你在代码运行时打印出来的一些数据和记录,是快速排查问题的好帮手,是撕逼和甩锅的利器!
327 0
|
缓存 Java 网络架构
别在 Java 代码里乱打日志了,这才是正确的打日志姿势!
别在 Java 代码里乱打日志了,这才是正确的打日志姿势!
134 0