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


目录
相关文章
|
6天前
|
设计模式 监控 Java
Java多线程基础-11:工厂模式及代码案例之线程池(一)
本文介绍了Java并发框架中的线程池工具,特别是`java.util.concurrent`包中的`Executors`和`ThreadPoolExecutor`类。线程池通过预先创建并管理一组线程,可以提高多线程任务的效率和响应速度,减少线程创建和销毁的开销。
25 2
|
2天前
|
存储 并行计算 安全
Java语言编写代码的技术性探讨
Java语言编写代码的技术性探讨
|
2天前
|
安全 Java API
Java语言中的代码安全性探讨
Java语言中的代码安全性探讨
|
4天前
|
Java C++
如何在JAVA代码中嵌入汇编
如何在JAVA代码中嵌入汇编
8 1
|
4天前
|
Java Linux
Linux下如何定位最耗CPU的JAVA代码
Linux下如何定位最耗CPU的JAVA代码
10 0
|
4天前
|
存储 Java 编译器
Java方法的基本内存原理与代码实例
Java方法的基本内存原理与代码实例
12 0
|
6天前
|
存储 Java 调度
Java多线程基础-11:工厂模式及代码案例之线程池(二)
这篇内容介绍了Java多线程基础,特别是线程池中的定时器和拒绝策略。
19 0
|
18天前
|
Java
使用Java代码打印log日志
使用Java代码打印log日志
258 1
|
Java BI API
在Java代码中打日志需要注意什么?
日志是什么?日志是你在代码运行时打印出来的一些数据和记录,是快速排查问题的好帮手,是撕逼和甩锅的利器!
339 0
|
缓存 Java 网络架构
别在 Java 代码里乱打日志了,这才是正确的打日志姿势!
别在 Java 代码里乱打日志了,这才是正确的打日志姿势!
138 0