在 Java 中使用计划执行器而不是硬循环来监视目录

Watch directory using scheduled executor instead of hard loop in Java

在我的 I was warking on a simple exercise that watched a directory for file changes. I took the code from this oracle docs 中,它没有任何问题,除了我不确定的一点未经检查的转换警告。

我对这段代码的下一个问题是它在线程中放置了一个硬循环,这至少在理论上是阻塞的。现在,我知道如果操作系统使用时间切片,即使是硬循环也被分成小块,与其他线程共享处理器时间,应用程序 运行ning,事实上我可以做各种不同线程中的硬循环 运行ning 不会相互阻塞的示例(只要它们具有相同的优先级),即使在明确创建的只有一个内核的虚拟机上也是如此。

但是,Java 语言不保证它使用哪种调度方式进行线程管理,是时间片还是循环;这取决于实际的 VM 实现和操作系统。因此,我在研究该主题时得到的建议是编写代码,就好像它必须 运行 在循环线程调度上一样,从而避免在线程中放置硬循环,除非我的代码可以连续收回控制权使用 sleep()wait()yeld() 等传递给其他线程(我可以想到一个 GUI,其中主线程是一个具有硬循环监视事件并发送控制的线程返回给听众来处理它们)。

然而,在我的例子中,我想不出一种方法让线程在处理完文件更改后进入休眠状态,或者将控制权交还给主循环,因为核心思想基本上是不断询问文件系统是否有变化。所以我想到的是一个定期调用监视线程的预定执行程序。显然,这是在拥有 "blocking" 线程与在文件系统发生更改时立即收到通知之间的权衡。由于在实际情况下我将进行此练习,我可能不需要立即通知,对此我很满意。代码非常简单:

// imports...
public class Main
{
    public static FileSystem fs;
    public static Path dir;
    public static WatchService watcher;
    public static WatchKey key;

    public static void main(String[] args)
    {
        fs = FileSystem.getDefault();
        dir = fs.getPath(".");

        try {
            watcher = fs.newWatchService();
            dir.register(watcher, StandardWatchEventKinds.ENTRY_MODIFY);
        } catch (IOException e) {
            System.err.println(e.getMessage());
            return;
        }

        Executors.newScheduledThreadPool(1).scheduleAtFixedRate(new Runnable()
            {
                public void run()
                {
                    Main.key = Main.watcher.poll();
                    if (null == Main.key)
                        return;

                    for (WatchEvent<?> event : Main.key.pollEvents()) {
                        WatchEvent.Kind<?> kind = event.kind();
                        if (kind == StandardWatchEventKinds.OVERFLOW)
                            continue;

                        @SuppressWarnings("unchecked");
                        WatchEvent<Path> ev = (WatchEvent<Path>)event;
                        Path file = ev.context();
                        System.out.println(file);

                        boolean valid = Main.key.reset();
                        if (!valid)
                            System.err.println("Invalid key!");
                    }
                }
            }, 0, 1, TimeUnit.SECONDS);
    }
}

所以我的问题是:

  1. 我是不是太过分了?我的意思是,如此关心线程中的阻塞代码实际上是一个好习惯,还是不存在时间分片的真实情况如此罕见,以至于我可以安全地在线程中放置一个硬循环,也许可以这样做仅当我知道我的代码将 运行 可能在具有保证循环法的嵌入式设备上时才会出现这种情况?

  2. 在这种特殊情况下,还有其他方法可以避免硬循环吗?可能是一些我想不到的巧妙使用线程控制的方法(sleep()wait()等)?

非常感谢,抱歉这么长post。

这是一个如何在后台线程中查看目录的示例。就是修改后的Java Tutorials Code Sample – WatchDir.java referenced by Oracle's The Java Tutorials: Watching a Directory for Changes.

重要的部分是watcher.take()。此处调用线程阻塞,直到发出键信号。因此,与您的代码片段相反,这些是这种方法的好处:

  1. 线程 'parked' 在 watcher.take() 中等待。没有 CPU cycles/resources 被浪费在等待中。 CPU 可以同时做其他事情。
  2. watcher.take() returns 当文件系统被修改时立即。 (您的代码在最坏情况下会在 1 秒后做出反应,在一般情况下会在 0.5 秒后做出反应。)

在main方法中,DirWatcher被实例化,运行被单线程ExecutorService实例化。此示例等待 10 秒,然后关闭观察程序和执行程序服务。

public class DirWatcher implements Runnable {

    private final Path dir;
    private final WatchService watcher;
    private final WatchKey key;

    @SuppressWarnings("unchecked")
    static <T> WatchEvent<T> cast(WatchEvent<?> event) {
        return (WatchEvent<T>) event;
    }

    /**
     * Creates a WatchService and registers the given directory
     */
    public DirWatcher(Path dir) throws IOException {
        this.dir = dir;
        this.watcher = FileSystems.getDefault().newWatchService();
        this.key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
    }

    public void run() {
        try {
            for (;;) {
                // wait for key to be signalled
                WatchKey key = watcher.take();

                if (this.key != key) {
                    System.err.println("WatchKey not recognized!");
                    continue;
                }

                for (WatchEvent<?> event : key.pollEvents()) {
                    WatchEvent<Path> ev = cast(event);
                    System.out.format("%s: %s\n", ev.kind(), dir.resolve(ev.context()));
                    // TODO: handle event. E.g. call listeners
                }

                // reset key
                if (!key.reset()) {
                    break;
                }
            }
        } catch (InterruptedException x) {
            return;
        }
    }

    public static void main(String[] args) throws IOException, InterruptedException, ExecutionException,
            TimeoutException {

        Path dir = Paths.get("C:\temp");
        DirWatcher watcher = new DirWatcher(dir);

        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future<?> future = executor.submit(watcher);
        executor.shutdown();

        // Now, the watcher runs in parallel
        // Do other stuff here

        // Shutdown after 10 seconds
        executor.awaitTermination(10, TimeUnit.SECONDS);
        // abort watcher
        future.cancel(true);

        executor.awaitTermination(1, TimeUnit.SECONDS);
        executor.shutdownNow();
    }
}

正如评论中指出的那样,take() 方法在提供新密钥之前不会阻止线程执行,但它使用类似于 wait() 方法的机制来放置线程休眠。

我也找到了this post where it's pointed out that the WatcherService exploit the native file event notification mechanism, if available (with a notable exception being the OSX implementation).

所以,为了回答我自己的问题,在硬循环中使用 take() 不会阻塞线程,因为它会自动让线程休眠,让其他线程使用 CPU,直到操作系统发出文件更改通知。