作者:FIN技术铺
文章分类:研发技术工具/SONAR扫描 投稿日期: 2024-7-19
SONAR系统基于项目配置的扫描规则识别交付代码当中存在的问题。本文针对日常扫描环节经常出现的问题进行梳理,盘点日常用户支持过程中发现典型案例,向用户解释为什么会被识别以及识别之后如何修改的问题。文章从风格&格式类、OOP类、集合类等典型场景进行归类,盘点各类场景下的典型问题,针对被规则命中的问题进行解释、基于示例说明原因并且给出解决办法。在与用户进行讨论过程当中,有意识就问题的根本原因进行分析记录,并摘选成文,供编码参考
01 并发处理类
1. 禁止在运行时显式创建线程;如果你需要通过线程来提升性能,请使用线程池
【解释】:在Java等编程语言中,显式创建线程是一种常见的做法,用于并发执行任务。但是,在实践中,过度使用显式线程创建可能会导致以下问题:
- 资源消耗:每个线程都需要分配一定的内存资源,如栈空间。大量线程的创建和销毁会导致资源消耗增加,甚至可能引发OutOfMemoryError。
- 性能下降:线程的创建和销毁都需要时间,如果频繁进行这些操作,会导致性能下降。
- 管理困难:大量的线程会增加线程管理的复杂性,容易导致线程间同步和通信的问题。
为了解决这些问题,推荐使用线程池来管理线程。线程池是一种用于管理和优化线程使用的技术。以下是使用线程池的一些优点:
- 资源复用:线程池中的线程可以被多个任务复用,避免了频繁创建和销毁线程带来的开销。
- 性能提升:由于线程的创建和销毁被减少,系统性能得到提升。
- 易于管理:线程池提供了一种统一的方式来管理线程,使得线程的同步和通信更加简单。
下面是一个使用Java中的ExecutorService(一种线程池实现)的示例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建一个固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
// 提交任务给线程池执行
for (int i = 0; i < 10; i++) {
executor.submit(() -> {
// 在这里编写任务的代码
System.out.println("Task executed by " + Thread.currentThread().getName());
});
}
// 关闭线程池
executor.shutdown();
}
}
在这个示例中,我们创建了一个固定大小为5的线程池,然后提交了10个任务给线程池执行。由于线程池的大小是固定的,所以同时执行的任务数量最多为5个。当一个任务完成后,线程池会复用该线程来执行下一个任务,直到所有任务都被执行完毕。最后,我们调用shutdown方法来关闭线程池。
2. 禁止在运行时显式创建线程池;线程池应该是一个公共对象,并在项目启动时初始化
【解释】:在软件开发中,避免在运行时显式创建线程池,并将其作为一个公共对象在项目启动时初始化,是一个较好的实践,理由如下:
- 资源管理:
- 在运行时频繁地创建和销毁线程池会导致资源的不必要消耗。线程池的创建涉及到分配内存和其他系统资源,这些操作都是有成本的。
- 通过在项目启动时初始化线程池,可以确保资源被有效地管理和复用,从而减少资源浪费。
- 性能优化:
- 线程池的创建和销毁都有一定的时间成本。如果每次需要执行并发任务时都创建一个新的线程池,这会导致性能下降。
- 使用预先初始化的线程池可以避免这种性能开销,因为线程池中的线程可以被多个任务复用。
- 配置一致性:
- 如果在代码的多个地方都显式创建线程池,很难确保所有线程池的配置都是一致的。这可能会导致系统行为的不一致性和不可预测性。
- 通过使用公共的、预先配置的线程池对象,可以确保整个应用程序中使用的是相同配置的线程池,从而提高系统的稳定性和可维护性。
- 易于监控和调试:
- 当线程池是公共对象时,它更容易被监控和管理。可以更容易地跟踪线程池的状态、当前活动的线程数、队列中的任务数等。
- 这有助于在出现问题时快速定位和解决问题。
以下是一个示例,说明如何在项目启动时初始化一个公共的线程池对象:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Application {
// 公共的线程池对象
public static ExecutorService executor;
public static void main(String[] args) {
// 在项目启动时初始化线程池
executor = Executors.newFixedThreadPool(5);
// ... 其他应用程序代码 ...
}
}
在这个示例中,Application 类有一个公共的静态 ExecutorService 对象 executor。在项目启动时(在 main 方法中),我们初始化这个 executor 对象为一个固定大小的线程池。之后,应用程序中的其他部分可以使用这个公共的 executor 对象来提交任务,而不需要自己创建新的线程池。
3. 禁止使用JDK的Executors创建线程池,应该通过ThreadPoolExecutor的方式创建
【解释】:在Java中,java.util.concurrent.Executors 类提供了一些便捷的工厂方法来创建线程池。然而,在实际生产环境中,通常推荐使用 ThreadPoolExecutor 类直接创建线程池,而不是使用 Executors 类。以下是几个原因:
- 可配置性:
- 使用 ThreadPoolExecutor 可以更灵活地配置线程池的各种参数,如核心线程数、最大线程数、线程空闲时间、任务队列等。
- 相比之下,Executors 提供的工厂方法创建的线程池往往具有固定的配置,可能无法满足特定的性能或资源需求。
- 资源控制:
- Executors 的某些方法(如 newCachedThreadPool)创建的线程池可能会无限制地创建新线程,这在高负载情况下可能导致资源耗尽。
- 通过 ThreadPoolExecutor,你可以明确设置最大线程数,从而防止系统资源的过度消耗。
- 队列选择:
- ThreadPoolExecutor 允许你选择不同的任务队列实现,如 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue 等。
- 正确的队列选择对于线程池的性能和行为至关重要。例如,一个有界队列可以帮助防止任务堆积,而无界队列在不当使用时可能导致内存耗尽。
- 线程工厂:
- ThreadPoolExecutor 允许你提供一个自定义的 ThreadFactory,这可以用来创建具有特定属性(如名称、优先级、守护状态等)的线程;这有助于更好地管理和监控线程。
- 拒绝策略:
- 当线程池无法处理更多任务时(例如,队列已满且所有线程都在工作),它需要一个策略来处理新提交的任务。
- ThreadPoolExecutor 允许你指定一个 RejectedExecutionHandler 来处理这种情况,而 Executors 的默认策略可能不适合所有用例。
以下是一个使用 ThreadPoolExecutor 创建线程池的示例:
import java.util.concurrent.*;
public class CustomThreadPoolExample {
public static void main(String[] args) {
// 创建一个具有自定义配置的 ThreadPoolExecutor
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // 核心线程数
10, // 最大线程数
60L, // 线程空闲时间(秒)
TimeUnit.SECONDS, // 时间单位
new ArrayBlockingQueue<>(100), // 任务队列
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
// 使用线程池执行任务...
}
}
在这个示例中,我们创建了一个具有自定义配置的 ThreadPoolExecutor。我们指定了核心和最大线程数、线程空闲时间、任务队列以及一个拒绝策略。这种方式的创建提供了更大的灵活性和控制力,使得线程池能更好地适应应用程序的需求。
4. 调度线程池(ScheduledThreadPoolExecutor)中的周期性任务要捕获Throwable异常
【解释】:在Java的 ScheduledThreadPoolExecutor 中调度周期性任务时,捕获 Throwable 异常是非常重要的。以下是几个原因:
- 防止任务终止:如果周期性任务抛出未捕获的异常,该任务可能会被提前终止,导致后续的计划执行被取消。通过捕获 Throwable,你可以确保即使任务出现异常,也不会中断其周期性执行。
- 资源泄露和系统稳定性:未捕获的异常可能导致资源泄露或其他不稳定行为。例如,如果异常导致线程终止,而线程持有某些资源(如数据库连接或文件句柄),则这些资源可能不会被适当释放。
- 错误处理和恢复:通过捕获异常,你可以实现自定义的错误处理逻辑。例如,你可能希望在出现异常时记录错误、通知管理员或尝试以不同的方式执行任务。
- 透明度和可诊断性:捕获并记录异常可以提高系统的可诊断性。通过查看日志或其他记录,你可以更容易地识别和解决问题。
以下示例,说明如何在 ScheduledThreadPoolExecutor 中捕获 Throwable 异常:
import java.util.concurrent.*;
public class ScheduledTaskExample {
public static void main(String[] args) {
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);
Runnable task = () -> {
try {
// 模拟一个可能抛出异常的任务
riskyOperation();
} catch (Throwable t) {
// 捕获并处理异常
System.err.println("Caught exception in scheduled task: " + t);
}
};
// 安排周期性任务
executor.scheduleAtFixedRate(task, 0, 10, TimeUnit.SECONDS);
}
private static void riskyOperation() throws Exception {
// 这里模拟一个可能抛出异常的操作
throw new Exception("Simulated exception in risky operation");
}
}
在这个示例中,我们创建了一个 ScheduledThreadPoolExecutor 并安排了一个周期性任务。任务中调用了 riskyOperation() 方法,该方法模拟了一个可能抛出异常的操作。通过使用 try-catch 块,我们捕获了任何可能由 riskyOperation() 方法抛出的 Throwable 异常,并打印了相应的错误消息。这样做可以确保即使出现异常,周期性任务也会继续执行,并且异常会被适当地处理。
5. JDK中基于AQS的并发对象ReentrantLock和CountDownLatch要在finally代码块中执行其逆向操作
【解释】:在JDK中,ReentrantLock 和 CountDownLatch 是基于 AbstractQueuedSynchronizer(AQS)的并发对象。当使用这些对象时,通常需要在 finally 代码块中执行其逆向操作。原因如下:
- 保证资源释放:无论是 ReentrantLock 的 lock() 和 unlock() 方法,还是 CountDownLatch 的 countDown() 方法,它们都涉及到对共享资源的操作。如果在获取资源后发生异常,而没有在 finally 块中释放资源,可能会导致资源泄露或其他线程无法正确访问资源。
- 异常安全性:在Java中,finally 块中的代码总是会被执行,无论 try 块中是否发生异常。因此,在 finally 块中释放资源可以确保资源总是被适当地管理,即使在异常情况下也是如此。
- 避免死锁:对于 ReentrantLock,如果在获取锁后没有在 finally 块中释放锁,可能会导致死锁。其他等待该锁的线程将永远被阻塞,因为锁永远不会被释放。
- 维持程序的正确性:对于 CountDownLatch,countDown() 方法用于减少计数器的值。如果在使用 CountDownLatch 时没有正确处理异常,可能会导致计数器的值不正确,从而影响程序的逻辑和并发行为。
以下是使用 ReentrantLock 的示例,说明为什么需要在 finally 块中释放锁:
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void performTask() {
lock.lock(); // 获取锁
try {
// 执行任务...
} finally {
lock.unlock(); // 在finally块中释放锁
}
}
}
在这个示例中,我们在 try 块中获取了锁,并在 finally 块中释放了锁。这样做可以确保无论 try 块中的代码是否成功执行或抛出异常,锁总是会被释放。
同样地,对于 CountDownLatch,也需要在适当的位置(通常是在 finally 块中)调用 countDown() 方法来确保计数器的正确管理。
6. ThreadLocal对象用完必须回收,否则可能造成内存泄露
【解释】:ThreadLocal 是 Java 提供的一个线程局部变量,它可以让每个线程都拥有自己的变量副本,这样就避免了多个线程之间共享变量的问题。然而,如果不正确使用 ThreadLocal,确实可能导致内存泄露。
以下是使用 ThreadLocal 可能导致内存泄露的原因:
- 引用链持续存在:当一个 ThreadLocal 变量被设置后,它会被当前线程持有。如果线程持续运行并不终止,那么 ThreadLocal 变量也不会被垃圾收集。特别是,在 Web 容器(如 Tomcat)中的线程池,线程可能会长时间存在,导致 ThreadLocal 变量持续存在。
- 无界的数据增长:如果代码持续地往 ThreadLocal 里放数据,但没有删除,那么数据会持续增长。例如,每次请求都往 ThreadLocal 里放数据,但请求结束后没有清理,那么随着请求的增加,内存消耗也会增加。
- 意外的线程共享:有时开发者可能误以为 ThreadLocal 的值是跨线程共享的,从而在不应该的地方使用它。这可能导致意外的对象保留和内存泄露。
- 静态引用:如果 ThreadLocal 被声明为静态的,并且其生命周期比应用本身还要长,那么它的生命周期就和应用的生命周期绑定了。这意味着,只要应用还在运行,ThreadLocal 就不会被垃圾收集。
以下是一个可能导致内存泄露的示例:
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void performTask() {
lock.lock(); // 获取锁
try {
// 执行任务...
} finally {
lock.unlock(); // 在finally块中释放锁
}
}
}
public class MemoryLeakExample {
private static ThreadLocal