为什么我的Java程序在启动后性能显著下降?

5
我正在编写一个小型的Java应用程序来分析大量的图像文件。目前,它通过计算图像中每个像素的亮度平均值并将其与文件夹中的其他图像进行比较,以找到文件夹中最亮的图像。
有时,启动后我可以得到100多张图像/秒的速率,但这几乎总会下降到<20张/秒,我不知道为什么。当它达到100+张/秒时,CPU使用率为100%,但接着就降至约20%,这似乎太低了。
以下是主要类:
public class ImageAnalysis {

    public static final ConcurrentLinkedQueue<File> queue = new ConcurrentLinkedQueue<>();
    private static final ConcurrentLinkedQueue<ImageResult> results = new ConcurrentLinkedQueue<>();
    private static int size;
    private static AtomicInteger running = new AtomicInteger();
    private static AtomicInteger completed = new AtomicInteger();
    private static long lastPrint = 0;
    private static int completedAtLastPrint;

    public static void main(String[] args){
        File rio = new File(IO.CAPTURES_DIRECTORY.getAbsolutePath() + File.separator + "Rio de Janeiro");

        String month = "12";

        Collections.addAll(queue, rio.listFiles((dir, name) -> {
            return (name.substring(0, 2).equals(month));
        }));

        size = queue.size();

        ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() + 1);

        for (int i = 0; i < 8; i++){
            AnalysisThread t = new AnalysisThread();
            t.setPriority(Thread.MAX_PRIORITY);
            executor.execute(t);
            running.incrementAndGet();
        }
    }

    public synchronized static void finished(){
        if (running.decrementAndGet() <= 0){

            ImageResult max = new ImageResult(null, 0);

            for (ImageResult r : results){
                if (r.averageBrightness > max.averageBrightness){
                    max = r;
                }
            }

            System.out.println("Max Red: " + max.averageBrightness + " File: " + max.file.getAbsolutePath());
        }
    }

    public synchronized static void finishedImage(ImageResult result){
        results.add(result);
        int c = completed.incrementAndGet();

        if (System.currentTimeMillis() - lastPrint > 10000){
            System.out.println("Completed: " + c + " / " + size + " = " + ((double) c / (double) size) * 100 + "%");
            System.out.println("Rate: " + ((double) c - (double) completedAtLastPrint) / 10D + " images / sec");
            completedAtLastPrint = c;

            lastPrint = System.currentTimeMillis();
        }
    }

}

还有线程类:

public class AnalysisThread extends Thread {

    @Override
    public void run() {
        while(!ImageAnalysis.queue.isEmpty()) {
            File f = ImageAnalysis.queue.poll();

            BufferedImage image;
            try {
                image = ImageIO.read(f);

                double color = 0;
                for (int x = 0; x < image.getWidth(); x++) {
                    for (int y = 0; y < image.getHeight(); y++) {
                        //Color c = new Color(image.getRGB(x, y));
                        color += image.getRGB(x,y);
                    }
                }

                color /= (image.getWidth() * image.getHeight());

                ImageAnalysis.finishedImage((new ImageResult(f, color)));

            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        ImageAnalysis.finished();
    }
}

您可以使用像jvisualvm这样的分析器来运行您的应用程序,并使用其采样器来测量哪个方法花费了最多的时间。此外,尝试在单个线程中运行它并观察任何差异。线程可能会在某些操作上相互阻塞。 - Utku Özdemir
2
你的类继承了 Thread,并设置了它的优先级,但是你通过 Executor 运行它,而 Executor 只将其视为 Runnable,所以这一切都是徒劳的。如果你想影响线程的优先级,你需要定义一个 ThreadFactory。否则,你的类也可以只实现 Runnable - user207421
你尝试过使用 ImageIO.setUseCache(false); 吗? - user3707125
@EJP 要让事情更无望的是,提高线程优先级只是一个提示,除非你是“管理员”或“root”,否则会被忽略。 - Peter Lawrey
3个回答

8
您似乎混淆了使用线程池和自己创建线程的方法。我建议您只使用其中一种方法,最好只使用固定线程池。很可能发生的情况是,您的线程会出现异常,但该异常被忽略,从而导致任务失败并杀死线程。
我建议您只使用线程池,不要尝试创建自己的线程或队列,因为这正是ExecutorService为您做的事情。对于每个任务,请将其提交到线程池中,每个图像一个任务。如果您不打算检查任何任务的错误,我建议您捕获所有Throwable并记录它们,否则您可能会遇到RuntimeExcepionError,却毫不知情。
如果您使用Java 8,更简单的方法是使用parallelStream()。您可以使用它来同时分析图像并收集结果,而无需分割工作和收集结果。例如:
List<ImageResults> results = Stream.of(rio.listFiles())
                                   .parallel()
                                   .filter(f -> checkFile(f))
                                   .map(f -> getResultsFor(f))
                                   .list(Collectors.toList());

与我得到的结果有些相似,parallelStream()方法在前3000张图像上进展非常迅速(80-90%的CPU使用率),然后经历了显着的减速,其中CPU使用率降至10-20%左右。 - Brian Voter
@BrianVoter 我会使用分析工具来查看资源使用情况。听起来你可能有一个内存泄漏的问题。当进程变慢时,总的内存使用量是多少?当 CPU 减速时,你是否看到了显著的磁盘活动? - Peter Lawrey

3
我看到您可能遇到CPU使用率下降的两个原因:
  • 您的任务非常I/O密集(读取图像 - ImageIO.read(f));
  • 存在同步方法的线程争用,而您的线程访问这些方法;
此外,图像的大小可能会影响执行时间。
为了有效地利用并行性,我建议您重新设计应用程序,并将两种任务实现并提交给执行器:
  • 第一个任务(生产者)是I/O密集型的,将读取图像数据并将其排队以进行内存处理;
  • 其他任务(消费者)将检索并分析图像信息;
然后通过一些分析,您将能够确定生产者和消费者之间的正确比例。

谢谢你的回答。每个图像的大小都相同,供参考。我在想 - 并行处理是否会加快文件读取时间?这个因素似乎取决于磁盘,我的直觉告诉我使用并行处理不会更快,但显然我不知道该怎么做 :P - Brian Voter
@EJP,那么这个事实是否使得这个答案成为一个不好的方法,还是我误解了它? - Brian Voter
@EJP,尝试测试一下 - 你会感到惊讶的。当你从多个线程读取时,驱动程序可以将请求分组并提高整体性能,而在顺序单线程读取时,这种情况不会发生。对于我的本地机器,多线程读取结果提高了2倍至3倍的性能。 - user3707125

0
我看到的问题是,在你所寻求的高性能并发模型中使用队列并不是最优选择。在现代CPU设计中,使用队列并不理想。队列实现在头、尾和大小变量上存在写入争用。由于消费者和生产者之间的速度差异,特别是在高I/O情况下使用时,它们要么总是接近满,要么接近空。这导致了高水平的争用。此外,在Java中,队列是垃圾的重要来源。
我的建议是在设计代码时应用机械同步。你可以采用LMAX Disruptor,这是一个高性能的线程间消息传递库,旨在解决这个并发问题,是你可以拥有的最佳解决方案之一。
附加参考资料

http://lmax-exchange.github.io/disruptor/files/Disruptor-1.0.pdf

http://martinfowler.com/articles/lmax.html

https://dzone.com/articles/mechanical-sympathy

http://www.infoq.com/presentations/mechanical-sympathy


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接