常见 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


目录
相关文章
|
19天前
|
Java API 开发工具
【Azure Developer】Java代码实现获取Azure 资源的指标数据却报错 "invalid time interval input"
在使用 Java 调用虚拟机 API 获取指标数据时,因本地时区设置非 UTC,导致时间格式解析错误。解决方法是在代码中手动指定时区为 UTC,使用 `ZoneOffset.ofHours(0)` 并结合 `withOffsetSameInstant` 方法进行时区转换,从而避免因时区差异引发的时间格式问题。
116 3
|
28天前
|
缓存 Java 开发者
Java 开发者必看!ArrayList 和 LinkedList 的性能厮杀:选错一次,代码慢成蜗牛
本文深入解析了 Java 中 ArrayList 和 LinkedList 的性能差异,揭示了它们在不同操作下的表现。通过对比随机访问、插入、删除等操作的效率,指出 ArrayList 在多数场景下更高效,而 LinkedList 仅在特定情况下表现优异。文章强调选择合适容器对程序性能的重要性,并提供了实用的选择法则。
102 3
|
2月前
|
人工智能 监控 安全
智慧工地解决方案,java智慧工地程序代码
智慧工地系统融合物联网、AI、大数据等技术,实现对施工现场“人、机、料、法、环”的全面智能监控与管理,提升安全、效率与决策水平。
|
2月前
|
算法 IDE Java
Java 项目实战之实际代码实现与测试调试全过程详解
本文详细讲解了Java项目的实战开发流程,涵盖项目创建、代码实现(如计算器与汉诺塔问题)、单元测试(使用JUnit)及调试技巧(如断点调试与异常排查),帮助开发者掌握从编码到测试调试的完整技能,提升Java开发实战能力。
261 0
|
3月前
|
安全 Java 测试技术
Java 项目实战中现代技术栈下代码实现与测试调试的完整流程
本文介绍基于Java 17和Spring技术栈的现代化项目开发实践。项目采用Gradle构建工具,实现模块化DDD分层架构,结合Spring WebFlux开发响应式API,并应用Record、Sealed Class等新特性。测试策略涵盖JUnit单元测试和Testcontainers集成测试,通过JFR和OpenTelemetry实现性能监控。部署阶段采用Docker容器化和Kubernetes编排,同时展示异步处理和反应式编程的性能优化。整套方案体现了现代Java开发的最佳实践,包括代码实现、测试调试
133 0
|
3月前
|
Java 编译器 数据库连接
Java异常处理:写出更健壮的代码
Java异常处理:写出更健壮的代码
160 0
|
13天前
|
数据采集 存储 弹性计算
高并发Java爬虫的瓶颈分析与动态线程优化方案
高并发Java爬虫的瓶颈分析与动态线程优化方案
Java 数据库 Spring
48 0
|
25天前
|
算法 Java
Java多线程编程:实现线程间数据共享机制
以上就是Java中几种主要处理多线程序列化资源以及协调各自独立运行但需相互配合以完成任务threads 的技术手段与策略。正确应用上述技术将大大增强你程序稳定性与效率同时也降低bug出现率因此深刻理解每项技术背后理论至关重要.
59 16