private synchronized void scheduleBatchIfRequired() {

        // When a file is changed the filesystem can trigger two changes, one to change the file content
        // and another to change the file access time. To prevent a duplicate read we delay the read
        // a bit so we can have many changes during that delay period but with only one read of the file.
        if (isBatchScheduled.compareAndSet(false, true)) {
            LOGGER.info("Scheduling call to change listener for file {} in {}ms",
                    dirToWatch.toAbsolutePath().normalize(),
                    DELAY_BEFORE_FILE_READ_MS);

            CompletableFuture.delayedExecutor(DELAY_BEFORE_FILE_READ_MS, TimeUnit.MILLISECONDS)
                    .execute(() -> {
                        try {
                            synchronized (this) {
                                // Atomically swap out the set with an empty one
                                final List<SimpleWatchEvent> allEvents = new ArrayList<>();
                                queue.drainTo(allEvents);

                                LOGGER.debug(() -> LogUtil.message("Draining batch of {} events", allEvents.size()));
                                final Map<Path, List<SimpleWatchEvent>> groupedByPath = allEvents.stream()
                                        .collect(Collectors.groupingBy(
                                                SimpleWatchEvent::path,
                                                Collectors.toList()));

                                for (Entry<Path, List<SimpleWatchEvent>> entry : groupedByPath.entrySet()) {
                                    final Path path = entry.getKey();
                                    List<SimpleWatchEvent> eventsForPath = entry.getValue();
                                    LOGGER.debug("path: {}, simpleWatchEvents: {}", path, eventsForPath);

                                    if (NullSafe.hasItems(eventsForPath)) {
                                        eventsForPath = deDupEvents(eventsForPath);

                                        // Now fire all the de-duped events
                                        for (final SimpleWatchEvent event : eventsForPath) {
                                            final Path affectedFile = event.path;
                                            switch (event.eventType) {
                                                case MODIFY -> onEntryModify(affectedFile);
                                                case CREATE -> onEntryCreate(affectedFile);
                                                case DELETE -> onEntryDelete(affectedFile);
                                            }
                                        }
                                    }
                                }

                            }
                        } finally {
                            isBatchScheduled.set(false);
                        }
                    });
        }
    }
Starts the object. Called <i>before</i> the application becomes available.
stroom-util/src/main/java/stroom/util/io/AbstractDirChangeMonitor.java