使用预定的执行程序而不是 Java 中的硬循环监视目录

swa*_*nee 0 java multithreading nonblocking watch executorservice

在我之前的问题中,我正在做一个简单的练习,观察目录中的文件更改。我从这个 oracle docs 中获取了代码,它没有问题,除了我不确定的小未经检查的强制转换警告。

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

但是,Java 语言不保证它使用哪种调度来管理线程,如果是时间片还是轮询;这取决于实际的 VM 实现和操作系统。所以我在学习这个主题时得到的建议是编写代码,就好像它必须在循环线程调度上运行一样,从而避免在线程中放置硬循环,除非我的代码可以不断地将控制权交还给其他线程与sleep()wait()yeld()等(我可以想到一个 GUI,其中主线程是一个硬循环监视事件,并将控制权发送回侦听器来处理它们的线程)。

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

// 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

所以我的问题是:

  1. 我把这推得太远了吗?我的意思是,非常关心线程中的阻塞代码实际上是一个好习惯,还是不存在时间切片的真实情况,如此罕见,以至于我可以安全地在线程中放置一个硬循环,也许可以这样做只有当我知道我的代码可能会在具有保证循环的嵌入式设备上运行时才需要这种东西?

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

非常感谢,很抱歉发了这么长的帖子。

isn*_*bad 5

这是一个如何在后台线程中查看目录的示例。它是Oracle 的 The Java Tutorials: Watching a Directory for Changes引用的修改后的Java 教程代码示例 – WatchDir.java

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

  1. 线程在 中等待时“停放” watcher.take()。等待时不会浪费 CPU 周期/资源。在此期间,CPU 可以做其他事情。
  2. watcher.take()当文件系统被修改时立即返回。(您的代码在最坏情况下会在 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();
    }
}
Run Code Online (Sandbox Code Playgroud)