【问题标题】:Efficient way to iterate over list of files迭代文件列表的有效方法
【发布时间】:2012-04-11 15:59:12
【问题描述】:

我正在寻找一种有效的方法来迭代一个或多个目录中的数千个文件。

迭代目录中文件的唯一方法似乎是File.list*() 函数。这些函数有效地加载某种集合中的整个文件列表,然后让用户对其进行迭代。就时间/内存消耗而言,这似乎是不切实际的。我尝试查看 commons-io 和其他类似工具。但他们最终都在里面的某个地方打电话给File.list*()。 JDK7 的walkFileTree() 接近了,但我无法控制何时选择下一个元素。

我在一个目录中有超过 150,000 个文件,经过多次 -Xms/-Xmm 试用后,我摆脱了内存溢出问题。但是填充数组所需的时间没有改变。

我希望制作某种 Iterable 类,它使用类似 opendir()/closedir() 的函数来根据需要延迟加载文件名。有没有办法做到这一点?

更新:

Java 7 NIO.2 支持通过java.nio.file.DirectoryStream 进行文件迭代。这是一个Iterable 类。至于JDK6及以下,唯一的选择是File.list*()方法。

【问题讨论】:

  • 我不知道是否存在标准解决方案。我想没有其他方法可以做到这一点,但你自己在 C 中实现它并通过 JNI 访问它......
  • 这个问题的答案可能会有所帮助 - stackoverflow.com/questions/1034977/…
  • 我怀疑这里真正的问题是您有一个包含 150K 文件的目录。我当然不想以这种方式对文件系统进行压力测试。你能不能不使用子目录,也许按文件名中的前两个字符对文件进行分组?
  • @DilumRanatunga:多年的经验告诉我,修复代码比要求用户改变工作方式更具成本效益:)
  • 文件名之间是否有共同的模式?

标签: java


【解决方案1】:

这里是一个示例,说明如何迭代目录条目而无需将 159k 的条目存储在数组中。根据需要添加错误/异常/关闭/超时处理。这种技术使用辅助线程来加载一个小的阻塞队列。

用法是:

FileWalker z = new FileWalker(new File("\\"), 1024); // start path, queue size
Iterator<Path> i = z.iterator();
while (i.hasNext()) {
  Path p = i.next();
}

例子:

public class FileWalker implements Iterator<Path> {
  final BlockingQueue<Path> bq;
  FileWalker(final File fileStart, final int size) throws Exception {
  bq = new ArrayBlockingQueue<Path>(size);
  Thread thread = new Thread(new Runnable() {
    public void run() {
      try {
        Files.walkFileTree(fileStart.toPath(), new FileVisitor<Path>() {
          public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
            return FileVisitResult.CONTINUE;
          }
          public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
            try {
              bq.offer(file, 4242, TimeUnit.HOURS);
            } catch (InterruptedException e) {
              e.printStackTrace();
            }
            return FileVisitResult.CONTINUE;
          }
          public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
            return FileVisitResult.CONTINUE;
          }
          public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
            return FileVisitResult.CONTINUE;
          }
        });
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  });
  thread.setDaemon(true);
  thread.start();
  thread.join(200);
}
public Iterator<Path> iterator() {
  return this;
}
public boolean hasNext() {
  boolean hasNext = false;
  long dropDeadMS = System.currentTimeMillis() + 2000;
  while (System.currentTimeMillis() < dropDeadMS) {
    if (bq.peek() != null) {
      hasNext = true;
      break;
    }
    try {
      Thread.sleep(1);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
  return hasNext;
}
public Path next() {
  Path path = null;
  try {
    path = bq.take();
  } catch (InterruptedException e) {
    e.printStackTrace();
  }
  return path;
}
public void remove() {
  throw new UnsupportedOperationException();
}
}

【讨论】:

  • 谢谢!额外的线程部分有点麻烦,但我会想办法将这个 Runnable 推送到一些无人机线程上。
  • @Eshan - 因为它死了,所以要付出很小的代价。但是请记住,如果您的 while(hasNext()) 提前终止,它将保持活动状态。如您所见,您需要添加一些故障安全代码。但是这种技术使内存使用率非常低。
【解决方案2】:

这在时间/内存消耗方面似乎不切实际。

即使 150,000 个文件也不会消耗不切实际的内存量。

我希望制作某种 Iterable 类,它使用类似 opendir()/closedir() 的函数来根据需要延迟加载文件名。有没有办法做到这一点?

您需要编写或查找本机代码库才能访问这些 C 函数。它可能会引入比它解决的问题更多的问题。我的建议是只使用 File.list() 并增加堆大小。


实际上,还有另一种 hacky 选择。使用System.exec 运行ls 命令(或Windows 等效命令)并编写您的迭代器以读取和解析命令输出文本。这避免了使用 Java 原生库带来的麻烦。

【讨论】:

  • 该软件在 15 年前设计时犯了一个错误,即分叉线程来执行当时的设计师认为它是“并行”的事情。今天的当前版本 fork 100+ 线程使用 1.5 GiB+ 内存在 JDK 6 上运行。目录列表只是增加了更多。我说的不切实际就是这个意思。 JNI/System.exec() 在这里不是一个选项。
  • “JNI/System.exec() 在这里不是一个选项”。那么你就没有使用 Java 6 的选项了。抱歉。
  • “该软件在 15 年前设计时犯了一个错误,即分叉线程来执行当时的设计师认为它是“并行”的事情。”。听起来你需要先解决这个问题。事实上,鉴于 JNI 和 exec 不是选项,您可能别无选择。但好消息是,您可能可以通过重组以使用有界线程池执行器服务来取代猖獗的线程分叉,从而消除 90 多个线程堆栈等的内存开销。
  • (或 C 计划……对用户的回击。“如果您想在大得离谱的目录上运行此应用程序,请购买具有大量内存的 64 位机器并在 64 位 JVM 中运行该应用程序。否则,它会崩溃。对不起。”。
  • 我们这样做已经有一段时间了。我感觉有点不对劲,主动清理代码并开始推动管理层允许我(和其他几个人)重写一些性能攻击性代码的主要部分。
【解决方案3】:

您能否按文件类型对负载进行分组以缩小批次范围?

【讨论】:

  • 跨目录分组拆分文件听起来不错。我在我的一个用户站点上尝试了这个,结果发现他们在两个目录中填充了数千个文件,一个是“a-z”,另一个是“0-9”。就像我在另一条评论中所说,修复代码比要求用户改变他们的工作方式更容易:)
【解决方案4】:

我只是想知道为什么一个普通的 file.list() 方法返回文件名的 String[] (而不是 file.listFiles() )会消耗大量内存?它是一个本机调用,只返回文件名。 也许你可以迭代它并延迟加载你需要的任何文件对象。

【讨论】:

  • 我的错。它是一个错字。应该是File.list()
  • 这正是查尔斯在他的回答中所写的。
猜你喜欢
  • 2022-10-19
  • 1970-01-01
  • 2023-03-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-03-06
  • 1970-01-01
相关资源
最近更新 更多