Java Fork / Join Framework

Java 7引入了一种称为Fork / Join Framework的新型ExecutorServiceJava Doc),它在处理递归算法方面表现出色。与的其他ExecutorService实现不同,Fork / Join框架使用工作窃取算法(Paper),该算法可最大程度地利用线程,并提供了一种更简单的方式来处理产生其他任务的任务(称为子任务)。

以下列出的所有代码都可以在以下网址获得:https : //github.com/javacreed/java-fork-join-example。大多数示例都不会包含完整的代码,并且可能会省略与所讨论的示例无关的片段。读者可以从上面的链接下载或查看所有代码。

本文简要介绍了所谓的传统执行程序服务(即所谓的执行程序服务)及其工作方式。然后,它介绍了Fork / Join框架,并描述了它与传统执行程序服务的区别。本文的第三部分显示了Fork / Join框架的实际示例,并演示了其大部分主要组件。

执行人服务

银行或邮局在分行设有几个柜台,向顾客提供服务。当柜台与当前客户一起完成时,队列中的下一个客户将接替其位置,柜台后面的人(也称为员工)开始为新客户提供服务。员工只会在给定的时间点为一个客户提供服务,排队的客户需要等待轮到他们。此外,员工非常耐心,即使他们在等待其他事情发生,也绝不会要求客户离开或离开。下图显示了队列中等待客户的简单视图以及为客户提供服务的员工的简单视图。

等待轮到的客户

类似的情况在多线程程序中也会发生,其中Threads(Java Doc)代表员工,而要执行的任务是客户。下图与上图相同,只是标签被更新以使用编程术语。

线程和任务

这应该有助于您将这两个方面联系起来,并更好地可视化所讨论的方案。

大多数线程池(Tutorial)和执行程序服务都以这种方式工作。为一个Thread分配了一项任务,并且只有在完成一个任务后,它才会移至下一个任务。任务可能需要很长时间才能完成,并且可能会阻止等待其他事情发生。在许多情况下,此方法效果很好,但由于需要递归解决的问题而严重失败。

让我们使用与客户在队列中等待时相同的类比。假设由员工1服务的客户1需要来自尚未排队的客户6的一些信息。他(她)(客户1)给他们的朋友(客户6)打电话,等待他(她)(客户6)来银行。同时,客户1停留在占用员工1的柜台上。如前所述,员工非常耐心,在解决所有依赖性之前,永远不会将客户送回队列或要求他们离开。 客户6 到达并排队,如下所示。

老顾客在等新顾客

客户1仍然占据员工的情况下,由于这个原因,其他客户,客户2客户3也一样(即等待排队的东西),因此我们陷入了僵局。所有员工都被等待发生事情的客户所占据。因此,员工将永远无法免费为其他客户提供服务。

在此示例中,我们看到了传​​统执行程序服务在处理任务时的弱点,而后者又取决于它们所创建的其他任务(称为子任务)。这在诸如河内塔(Wiki)之类的递归算法中或在探索类似数据结构的树(计算目录的总大小)中非常常见。Fork / Join框架旨在解决此类问题,我们将在下一部分中看到。在本文的后面,我们还将看到本节中讨论的问题的示例。

Fork / Join框架

传统执行程序服务实现在处理任务时(主要取决于其他子任务)的主要缺点是,线程无法将任务放回队列或侧边,然后服务/执行新任务。Fork / Join框架通过在任务和执行它们的线程之间引入另一层来解决此限制,该层允许线程将阻塞的任务放在一边,并在执行所有依赖项时对其进行处理。换句话说,如果任务1依赖于任务6,即任务1创建了哪个任务(任务6),则任务1放在一边,仅在任务6执行一次被执行。这将线程从任务1中释放出来,并允许它执行其他任务,这是传统执行程序服务实现无法实现的。

这是通过使用框架提供的forkjoin操作来实现的(因此命名为Fork / Join)。 任务1派生任务6,然后将其加入以等待结果。fork操作将Task 6放在队列中,而join操作允许线程1Task 1放在一边,直到Task 6完成。这就是fork / join的工作方式,fork将新事物推入队列,而join使当前任务处于支持状态,直到它可以继续进行,从而不阻塞线程。

Fork / Join框架使用一种称为ForkJoinPoolJava Doc)的特殊线程池,该线程池将其与其他线程池区分开来。 ForkJoinPool实现偷窃算法,并可以执行ForkJoinTaskJava Doc)对象。所述ForkJoinPool保持多个线程,其数量通常是基于可用的CPU的数量。每个线程都有一种特殊的队列Deques(Java Doc),所有任务都放置在该队列中。这是很重要的一点,要理解。线程不共享公共队列,但是每个线程都有自己的队列,如下所示。

线程及其队列

上面的图像说明了每个线程具有的另一个队列(图像的下部)。可以这样称呼它,它允许线程搁置被阻塞的任务,等待其他事情发生。换句话说,如果当前的任务不能继续(因为它执行一个连接上的子任务),然后将其放置在这个队列中,直到其所有相关的准备。

将新任务添加到线程的队列中(使用fork操作),并且每个线程始终处理添加到其队列中的最后一个任务。这很重要。如果线程的队列中有两个任务,则首先处理添加到队列中的最后一个任务。这称为后进先出LIFO(Wiki)。

任务1和任务2

在上图中,线程1的队列中有两个任务,其中任务1被添加到任务2之前的队列中。因此,任务2将首先由线程1执行,然后再执行任务1。任何空闲线程都可以从其他线程队列中获取任务(如果有的话),即工作窃取。线程将始终从其他线程的队列中窃取最早的任务,如下图所示。

任务2被线程2窃取

如上图所示,线程2线程1偷走了最早的任务Task 1。根据经验,线程将始终尝试从其相邻线程中进行窃取,以最大程度地减少工作窃取过程中可能产生的争用。

任务执行和被盗的顺序非常重要。理想情况下,窃取工作不会发生太多,因为这需要付出一定的代价。当任务从一个线程移动到另一个线程时,与此任务相关的上下文需要从一个线程的堆栈移动到另一个线程。线程可能(和Fork / Join框架将工作分散到所有CPU上)在另一个CPU上。将线程上下文从一个CPU移到另一个CPU可能会更慢。因此,Fork / Join Framework将其最小化,如下所述。

递归算法从一个大问题开始,然后应用分而治之的技术将问题分解为更小的部分,直到这些问题足够小以至于可以直接解决。添加到队列中的第一个任务是最大的任务。第一个任务会将问题分解为一组较小的任务,这些任务将添加到队列中,如下所示。

任务和子任务

任务1代表我们的问题,分为两个任务, 任务2小得可以按原样解决,但任务3需要进一步划分。任务任务4任务5足够小,不需要进一步拆分。这代表了一种典型的递归算法,可以将其拆分为较小的部分,然后在准备好时将结果汇总。这种算法的一个实际例子是计算目录的大小。我们知道目录的大小等于其文件的大小。

目录和文件

因此,目录1的大小等于文件2的大小加上目录3的大小。由于目录3是目录,因此其大小等于其内容的大小。换句话说,目录3的大小等于文件4的大小加上文件5的大小。

让我们看看这是如何执行的。我们从一项任务开始,即计算目录的大小,如下图所示。

第1步-从任务1开始

线程1将执行任务1,该任务将分叉另外两个子任务。这些任务将添加到线程1的队列中,如下图所示。

步骤2-添加子任务任务2和任务3

任务1正在等待子任务Task 2Task 3完成,因此被推到一边以释放线程1。为了使用更好的术语,任务1将子任务Task 2Task 3合并在一起。 线程1开始执行任务3(最后一个添加到其队列中的任务),而线程2则窃取任务2

步骤3-线程2窃取任务2

请注意,线程1已经开始处理其第二项任务,而线程3仍处于空闲状态。正如我们稍后将看到的,线程将不会执行相同数量的工作,并且第一个线程将始终比最后一个线程产生更多的工作。 任务3分叉了另外两个子任务,这些子任务被添加到正在执行该任务的线程的队列中。因此,另外两个任务被添加到线程1的队列中。 从任务2准备好的线程2再次窃取了另一个任务,如下所示。

步骤4-线程2窃取任务4

在上面的示例中,我们看到线程3从未执行过任务。这是因为我们只有很少的子任务。一旦任务4任务5准备就绪,它们的结果将用于计算任务3,然后计算任务1

如前所述,工作没有在线程之间平均分配。下表显示了在计算相当大的目录的大小时工作如何在线程之间分配。

工作分配

在上面的示例中,使用了四个线程。正如预期的那样,线程1执行了将近40%的工作,而线程4(最后一个线程)执行了略多于5%的工作。这是另一个重要的原理。Fork / Join框架不会在线程之间平均分配工作,并且将尝试最小化使用的线程数。第二个线程将仅从第一个线程开始工作,因为这无法满足要求。如前所述,在线程之间移动任务具有使框架尽量减少的成本。

本节详细介绍了Fork / Join框架的工作方式以及线程如何从其他线程的队列中窃取工作。在以下部分中,我们将看到Fork / Join框架的几个实际示例,并将分析获得的结果。

计算目录总大小

为了演示Fork / Join Framework的用法,我们将计算目录的大小,该问题可以递归解决。文件大小可以通过方法length()Java Doc)确定。目录的大小等于其所有文件的大小。

我们将使用几种方法来计算目录的大小,其中一些方法将使用Fork / Join Framework,并且我们将分析每种情况下获得的结果。

使用单线程(无并发)

第一个示例将不使用线程,而是简单地定义所使用的算法。

package com.javacreed.examples.concurrency.part1;

import java.io.File;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DirSize {

  private static final Logger LOGGER = LoggerFactory.getLogger(DirSize.class);

  public static long sizeOf(final File file) {
    DirSize.LOGGER.debug("Computing size of: {}", file);

    long size = 0;

    /* Ignore files which are not files and dirs */
    if (file.isFile()) {
      size = file.length();
    } else {
      final File[] children = file.listFiles();
      if (children != null) {
        for (final File child : children) {
          size += DirSize.sizeOf(child);
        }
      }
    }

    return size;
  }
}

该类DirSize 有一个称为的方法sizeOf(),该方法以FileJava Doc)实例作为参数。如果此实例是文件,则该方法返回文件的长度,否则,如果它是目录,则此方法为目录sizeOf()中的每个文件调用该方法,并返回总大小。

下面的示例演示如何使用FilePath.TEST_DIR常量定义的文件路径来运行此示例。

package com.javacreed.examples.concurrency.part1;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.javacreed.examples.concurrency.utils.FilePath;

public class Example1 {

  private static final Logger LOGGER = LoggerFactory.getLogger(Example1.class);

  public static void main(final String[] args) {
    final long start = System.nanoTime();
    final long size = DirSize.sizeOf(FilePath.TEST_DIR);
    final long taken = System.nanoTime() - start;

    Example1.LOGGER.debug("Size of '{}': {} bytes (in {} nano)", FilePath.TEST_DIR, size, taken);
  }
}

上面的示例将计算目录的大小,并在打印总大小之前打印所有访问的文件。以下片段仅显示最后一行,即测试文件夹()下的downloads目录的大小以及C:\Test计算该大小所花费的时间。

...
16:55:38.045 [main] INFO Example1.java:38 - Size of 'C:\Test\': 113463195117 bytes (in 4503253988 nano)

要为访问的每个文件禁用日志,只需将日志级别更改为INFO(在中log4j.properties),日志将仅显示最终结果。

log4j.rootCategory=warn, R
log4j.logger.com.javacreed=info, stdout

请注意,日志记录只会使事情变慢。实际上,如果您运行的示例中没有日志(或将日志设置为INFO),则目录大小的计算要快得多。

为了获得更可靠的结果,我们将多次运行相同的测试,然后返回如下所示的平均时间。

package com.javacreed.examples.concurrency.part1;

import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.javacreed.examples.concurrency.utils.FilePath;
import com.javacreed.examples.concurrency.utils.Results;

public class Example2 {

  private static final Logger LOGGER = LoggerFactory.getLogger(Example1.class);

  public static void main(final String[] args) {
    final Results results = new Results();
    for (int i = 0; i < 5; i++) {
      results.startTime();
      final long size = DirSize.sizeOf(FilePath.TEST_DIR);
      final long taken = results.endTime();
      Example2.LOGGER.info("Size of '{}': {} bytes (in {} nano)", FilePath.TEST_DIR, size, taken);
    }

    final long takenInNano = results.getAverageTime();
    Example2.LOGGER.info("Average: {} nano ({} seconds)", takenInNano, TimeUnit.NANOSECONDS.toSeconds(takenInNano));
  }
}

相同的测试执行五次,平均结果最后打印如下。

16:58:00.496 [main] INFO Example2.java:42 - Size of 'C:\Test\': 113463195117 bytes (in 4266090211 nano)
16:58:04.728 [main] INFO Example2.java:42 - Size of 'C:\Test\': 113463195117 bytes (in 4228931534 nano)
16:58:08.947 [main] INFO Example2.java:42 - Size of 'C:\Test\': 113463195117 bytes (in 4224277634 nano)
16:58:13.205 [main] INFO Example2.java:42 - Size of 'C:\Test\': 113463195117 bytes (in 4253856753 nano)
16:58:17.439 [main] INFO Example2.java:42 - Size of 'C:\Test\': 113463195117 bytes (in 4235732903 nano)
16:58:17.439 [main] INFO Example2.java:46 - Average: 4241777807 nano (4 seconds)

递归任务

Fork / Join框架提供两种类型的任务,RecursiveTaskJava Doc)和RecursiveActionJava Doc)。在本节中,我们仅讨论RecursiveTaskRecursiveAction稍后将讨论。

ARecursiveTask是一项在执行时会返回值的任务。因此,此类任务将返回计算结果。在我们的例子中,任务返回它代表的文件或目录的大小。对该类DirSize进行了修改以使用,RecursiveTask如下所示。

package com.javacreed.examples.concurrency.part2;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DirSize {

  private static final Logger LOGGER = LoggerFactory.getLogger(DirSize.class);

  private static class SizeOfFileTask extends RecursiveTask<Long> {

    private static final long serialVersionUID = -196522408291343951L;

    private final File file;

    public SizeOfFileTask(final File file) {
      this.file = Objects.requireNonNull(file);
    }

    @Override
    protected Long compute() {
      DirSize.LOGGER.debug("Computing size of: {}", file);

      if (file.isFile()) {
        return file.length();
      }

      final List<SizeOfFileTask> tasks = new ArrayList<>();
      final File[] children = file.listFiles();
      if (children != null) {
        for (final File child : children) {
          final SizeOfFileTask task = new SizeOfFileTask(child);
          task.fork();
          tasks.add(task);
        }
      }

      long size = 0;
      for (final SizeOfFileTask task : tasks) {
        size += task.join();
      }

      return size;
    }
  }

  public static long sizeOf(final File file) {
    final ForkJoinPool pool = new ForkJoinPool();
    try {
      return pool.invoke(new SizeOfFileTask(file));
    } finally {
      pool.shutdown();
    }
  }

  private DirSize() {}

}

让我们将该类分为几个小部分,分别进行描述。

  1. 该类具有私有构造函数,因为它不打算初始化。因此,没有必要初始化它(创建这种类型的对象),并且为了防止某人对其进行初始化,我们构造了构造函数private。所有方法都是静态的,可以直接针对该类调用这些方法。

       private DirSize() {}
    
  2. 该方法sizeOf()不计算文件或目录的大小。相反,它创建的实例ForkJoinPool并开始计算过程。它等待要计算的目录大小,最后在退出前关闭池。

       public static long sizeOf(final File file) {
         final ForkJoinPool pool = new ForkJoinPool();
         try {
           return pool.invoke(new SizeOfFileTask(file));
         } finally {
           pool.shutdown();
         }
       }
    

    ForkJoinPool默认情况下,由创建的线程是守护程序线程。一些文章建议不要关闭此池,因为这些线程不会阻止VM关闭。话虽如此,我建议在不再需要这些对象时,关闭它们并妥善处理。即使不再需要这些守护程序线程,它们也可能长时间闲置。

  3. 该方法sizeOf()创建一个实例SizeOfFileTask,该类扩展RecursiveTask<Long>。因此,invoke方法将返回此任务返回的对象/值。

           return pool.invoke(new SizeOfFileTask(file));
    

    请注意,以上代码将阻塞,直到计算出目录的大小为止。换句话说,上面的代码将等待任务(和所有子任务)完成工作,然后再继续。

  4. 该类SizeOfFileTask是该类中的内部DirSize类。

       private static class SizeOfFileTask extends RecursiveTask<Long> {
    
         private static final long serialVersionUID = -196522408291343951L;
    
         private final File file;
    
         public SizeOfFileTask(final File file) {
           this.file = Objects.requireNonNull(file);
         }
    
         @Override
         protected Long compute() {
           /* Removed for brevity */
         }
       }
    

    它使用要为其计算大小的文件(可以是目录)作为其唯一构造函数的参数,而不能是null。该compute()方法负责计算此任务的工作。在这种情况下,文件或目录的大小将在下面讨论。

  5. compute()方法确定传递给其构造函数的文件是文件还是目录,并采取相应的措施。

         @Override
         protected Long compute() {
           DirSize.LOGGER.debug("Computing size of: {}", file);
    
           if (file.isFile()) {
             return file.length();
           }
    
           final List<SizeOfFileTask> tasks = new ArrayList<>();
           final File[] children = file.listFiles();
           if (children != null) {
             for (final File child : children) {
               final SizeOfFileTask task = new SizeOfFileTask(child);
               task.fork();
               tasks.add(task);
             }
           }
    
           long size = 0;
           for (final SizeOfFileTask task : tasks) {
             size += task.join();
           }
    
           return size;
         }
    

    如果文件是文件,则该方法仅返回其大小,如下所示。

           if (file.isFile()) {
             return file.length();
           }
    

    否则,如果文件是目录,它将列出其所有子文件,并SizeOfFileTask为每个子文件创建一个新实例。

           final List<SizeOfFileTask> tasks = new ArrayList<>();
           final File[] children = file.listFiles();
           if (children != null) {
             for (final File child : children) {
               final SizeOfFileTask task = new SizeOfFileTask(child);
               task.fork();
               tasks.add(task);
             }
           }
    

    对于created的每个实例,将调用SizeOfFileTaskfork()方法。该fork()方法导致将新的实例SizeOfFileTask添加到此线程的队列中。创建的所有实例SizeOfFileTask都保存在名为的列表中tasks。最后,当所有任务分叉时,我们需要等待它们完成其值的总和。

           long size = 0;
           for (final SizeOfFileTask task : tasks) {
             size += task.join();
           }
    
           return size;
    

    这是通过该join()方法完成的。这个`join() will force this task to stop, step aside if needs be, and wait for the subtask to finish. The value returned by all subtasks is added to the value of the variable size`作为此目录的大小返回。

与不使用多线程的较简单版本相比,Fork / Join Framework更复杂。这是一个公平的观点,但是较简单的版本要慢4倍。Fork / Join示例平均花费一秒钟来计算大小,而非线程版本平均花费4秒,如下所示。

16:59:19.557 [main] INFO Example3.java:42 - Size of 'C:\Test\': 113463195117 bytes (in 2218013380 nano)
16:59:21.506 [main] INFO Example3.java:42 - Size of 'C:\Test\': 113463195117 bytes (in 1939781438 nano)
16:59:23.505 [main] INFO Example3.java:42 - Size of 'C:\Test\': 113463195117 bytes (in 2004837684 nano)
16:59:25.363 [main] INFO Example3.java:42 - Size of 'C:\Test\': 113463195117 bytes (in 1856820890 nano)
16:59:27.149 [main] INFO Example3.java:42 - Size of 'C:\Test\': 113463195117 bytes (in 1782364124 nano)
16:59:27.149 [main] INFO Example3.java:46 - Average: 1960363503 nano (1 seconds)

在本节中,我们看到了多线程如何帮助我们改善程序性能。在下一节中,我们将看到不适当使用多线程如何使情况变得更糟。

执行器服务

在前面的示例中,我们看到了并发如何提高算法的性能。如本节所述,如果滥用,多线程处理可能会导致较差的结果。我们将尝试使用传统的执行程序服务来解决此问题。

请注意,本节中显示的代码已损坏,无法正常工作。它会永远挂着,仅用于演示目的。

该类DirSize已修改为可用于ExecutorServiceCallableJava Doc)。

package com.javacreed.examples.concurrency.part3;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This example is broken and suffers from deadlock and is only included for documentation purpose.
 *
 * @author Albert Attard
 */
public class DirSize {

  private static class SizeOfFileCallable implements Callable<Long> {

    private final File file;
    private final ExecutorService executor;

    public SizeOfFileCallable(final File file, final ExecutorService executor) {
      this.file = Objects.requireNonNull(file);
      this.executor = Objects.requireNonNull(executor);
    }

    @Override
    public Long call() throws Exception {
      DirSize.LOGGER.debug("Computing size of: {}", file);
      long size = 0;

      if (file.isFile()) {
        size = file.length();
      } else {
        final List<Future<Long>> futures = new ArrayList<>();
        for (final File child : file.listFiles()) {
          futures.add(executor.submit(new SizeOfFileCallable(child, executor)));
        }

        for (final Future<Long> future : futures) {
          size += future.get();
        }
      }

      return size;
    }
  }

  public static <T> long sizeOf(final File file) {
    final int threads = Runtime.getRuntime().availableProcessors();
    DirSize.LOGGER.debug("Creating executor with {} threads", threads);
    final ExecutorService executor = Executors.newFixedThreadPool(threads);
    try {
      return executor.submit(new SizeOfFileCallable(file, executor)).get();
    } catch (final Exception e) {
      throw new RuntimeException("Failed to calculate the dir size", e);
    } finally {
      executor.shutdown();
    }
  }

  private static final Logger LOGGER = LoggerFactory.getLogger(DirSize.class);

  private DirSize() {}

}

这个想法与以前非常相似。内部类SizeOfFileCallable扩展Callable<Long>并将其子任务的计算委托给ExecutorService传递给其构造函数的实例。处理时不需要这样做RecursiveTask,因为后者会自动将其子类添加到线程的队列中以供执行。

我们将不进行详细介绍,以使本文重点关注Fork / Join框架。如前所述,一旦所有线程都被占用,此方法将阻塞,如下所示。

17:22:39.216 [main] DEBUG DirSize.java:78 - Creating executor with 4 threads
17:22:39.222 [pool-1-thread-1] DEBUG DirSize.java:56 - Computing size of: C:\Test\
17:22:39.223 [pool-1-thread-2] DEBUG DirSize.java:56 - Computing size of: C:\Test\Dir 1
17:22:39.223 [pool-1-thread-4] DEBUG DirSize.java:56 - Computing size of: C:\Test\Dir 2
17:22:39.223 [pool-1-thread-3] DEBUG DirSize.java:56 - Computing size of: C:\Test\Dir 3

此示例在Core i5计算机上执行,该计算机具有四个可用处理器(如Runtime.getRuntime().availableProcessors() Java Doc所示)。一旦所有四个线程都被占用,此方法将永远阻塞,就像我们在本文开头的bank分支示例中看到的那样。所有线程都被占用,因此不能用于解决其他任务。可以建议使用更多线程。虽然这似乎是一个解决方案,但Fork / Join Framework仅使用四个线程解决了相同的问题。此外,线程并不便宜,一个人不应仅仅因为他或她选择了不合适的技术而简单地产生了数千个线程。

尽管在编程社区中过度使用了多线程一词,但多线程技术的选择很重要,因为如上所见,某些选项在某些情况下根本不起作用。

递归动作

Fork / Join框架支持两种类型的任务。第二种任务是RecursiveAction。这些类型的任务并不意味着要返回任何东西。对于要执行某项操作(例如删除文件而不返回任何内容)的情况,这些方法是理想的选择。通常,您不能删除空目录。首先,您需要先删除其所有文件。在这种情况下,RecursiveAction可以使用,其中每个操作要么删除文件,要么先删除所有目录内容,然后再删除目录本身。

以下是本文中的最后一个示例。它显示了的修改版本,该版本DirSize利用SizeOfFileAction内部类来计算目录的大小。

package com.javacreed.examples.concurrency.part4;

import java.io.File;
import java.util.Objects;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveAction;
import java.util.concurrent.atomic.AtomicLong;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DirSize {

  private static class SizeOfFileAction extends RecursiveAction {

    private static final long serialVersionUID = -196522408291343951L;

    private final File file;
    private final AtomicLong sizeAccumulator;

    public SizeOfFileAction(final File file, final AtomicLong sizeAccumulator) {
      this.file = Objects.requireNonNull(file);
      this.sizeAccumulator = Objects.requireNonNull(sizeAccumulator);
    }

    @Override
    protected void compute() {
      DirSize.LOGGER.debug("Computing size of: {}", file);

      if (file.isFile()) {
        sizeAccumulator.addAndGet(file.length());
      } else {
        final File[] children = file.listFiles();
        if (children != null) {
          for (final File child : children) {
            ForkJoinTask.invokeAll(new SizeOfFileAction(child, sizeAccumulator));
          }
        }
      }
    }
  }

  public static long sizeOf(final File file) {
    final ForkJoinPool pool = new ForkJoinPool();
    try {
      final AtomicLong sizeAccumulator = new AtomicLong();
      pool.invoke(new SizeOfFileAction(file, sizeAccumulator));
      return sizeAccumulator.get();
    } finally {
      pool.shutdown();
    }
  }

  private static final Logger LOGGER = LoggerFactory.getLogger(DirSize.class);

  private DirSize() {}

}

此类与之前的类非常相似。主要区别在于返回最终值(文件或目录的大小)的方式。请记住,RecursiveAction不能返回值。而是,所有任务将共享一个类型的公共计数器,AtomicLong并且这些任务将增加该公共计数器的数量,而不是返回文件的大小。

让我们将该课程分为几个较小的部分,并逐一进行各个部分。我们将跳过之前已经解释过的部分,以免重复。

  1. 该方法sizeOf()利用了ForkJoinPool以前的方法。通用计数器(也称为sizeAccumulator)也用此方法初始化,并传递给第一个任务。该实例将与所有子任务共享,并且所有实例都会增加该值。

       public static long sizeOf(final File file) {
         final ForkJoinPool pool = new ForkJoinPool();
         try {
           final AtomicLong sizeAccumulator = new AtomicLong();
           pool.invoke(new SizeOfFileAction(file, sizeAccumulator));
           return sizeAccumulator.get();
         } finally {
           pool.shutdown();
         }
       }
    

    像以前一样,此方法将阻塞,直到所有子任务准备就绪,然后返回总大小。

  2. 内部类SizeOfFileAction扩展,RecursiveAction并且其构造函数接受两个参数。

       private static class SizeOfFileAction extends RecursiveAction {
    
         private static final long serialVersionUID = -196522408291343951L;
    
         private final File file;
         private final AtomicLong sizeAccumulator;
    
         public SizeOfFileAction(final File file, final AtomicLong sizeAccumulator) {
           this.file = Objects.requireNonNull(file);
           this.sizeAccumulator = Objects.requireNonNull(sizeAccumulator);
         }
    
         @Override
         protected void compute() {
           /* Removed for brevity */
         }
       }
    

    第一个参数是将要计算大小的文件(或目录)。第二个参数是共享计数器。

  3. 这里的计算方法稍微简单一些,因为它不必等待子任务。如果给定的文件是一个文件,则它将增加公共计数器(称为sizeAccumulator)。否则,如果此文件是目录,它将SizeOfFileAction为每个子文件派生新实例。

         protected void compute() {
           DirSize.LOGGER.debug("Computing size of: {}", file);
    
           if (file.isFile()) {
             sizeAccumulator.addAndGet(file.length());
           } else {
             final File[] children = file.listFiles();
             if (children != null) {
               for (final File child : children) {
                 ForkJoinTask.invokeAll(new SizeOfFileAction(child, sizeAccumulator));
               }
             }
           }
         }
    

    在这种情况下,方法invokeAll()Java Doc)用于派生任务。

这种方法大约需要11秒钟才能完成,使其成为所有三个方法中最慢的一个,如下所示。

19:04:39.925 [main] INFO Example5.java:40 - Size of 'C:\Test': 113463195117 bytes (in 11445506093 nano)
19:04:51.433 [main] INFO Example5.java:40 - Size of 'C:\Test': 113463195117 bytes (in 11504270600 nano)
19:05:02.876 [main] INFO Example5.java:40 - Size of 'C:\Test': 113463195117 bytes (in 11442215513 nano)
19:05:15.661 [main] INFO Example5.java:40 - Size of 'C:\Test': 113463195117 bytes (in 12784006599 nano)
19:05:27.089 [main] INFO Example5.java:40 - Size of 'C:\Test': 113463195117 bytes (in 11428115064 nano)
19:05:27.226 [main] INFO Example5.java:44 - Average: 11720822773 nano (11 seconds)

这可能使许多人感到惊讶。当使用多个线程时,这怎么可能?这是一个普遍的误解。多线程不能保证更好的性能。在这种情况下,我们存在设计缺陷,可以避免。命名的公用计数器sizeAccumulator在所有线程之间共享,因此导致线程之间的争用。这实际上在创建瓶颈时违反了分治法的目的。

结论

本文提供了有关Fork / Join框架以及如何使用它的详细说明。它提供了一个实例,并比较了几种方法。Fork / Join框架是递归算法的理想选择,但它无法在线程之间平均分配负载。任务和子任务除了加入外,不应阻塞其他任何事物,而应使用fork委派工作。避免在任务中进行任何阻塞的IO操作,并最小化可变共享状态,尤其是尽可能地修改变量,因为这会对整体性能产生负面影响。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值