Polish FileSystemWatcher and improve its thread safety

- Limit shared state between FileSystemWatcher and the watching thread
- Use a private monitor rather than synchronizing on this
- Use a Runnable implementation rather than subclassing Thread
- Synchronize consistently when reading and writing state

Closes gh-6039
pull/6118/head
Andy Wilkinson 9 years ago
parent 3772d9f937
commit f3e9f1e6e3

@ -21,6 +21,7 @@ import java.io.FileFilter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
@ -44,7 +45,7 @@ public class FileSystemWatcher {
private static final long DEFAULT_QUIET_PERIOD = 400;
private List<FileChangeListener> listeners = new ArrayList<FileChangeListener>();
private final List<FileChangeListener> listeners = new ArrayList<FileChangeListener>();
private final boolean daemon;
@ -52,14 +53,16 @@ public class FileSystemWatcher {
private final long quietPeriod;
private Thread watchThread;
private final AtomicInteger remainingScans = new AtomicInteger(-1);
private AtomicInteger remainingScans = new AtomicInteger(-1);
private final Map<File, FolderSnapshot> folders = new HashMap<File, FolderSnapshot>();
private Map<File, FolderSnapshot> folders = new LinkedHashMap<File, FolderSnapshot>();
private Thread watchThread;
private FileFilter triggerFilter;
private final Object monitor = new Object();
/**
* Create a new {@link FileSystemWatcher} instance.
*/
@ -89,11 +92,13 @@ public class FileSystemWatcher {
* {@link #start() started}.
* @param fileChangeListener the listener to add
*/
public synchronized void addListener(FileChangeListener fileChangeListener) {
public void addListener(FileChangeListener fileChangeListener) {
Assert.notNull(fileChangeListener, "FileChangeListener must not be null");
synchronized (this.monitor) {
checkNotStarted();
this.listeners.add(fileChangeListener);
}
}
/**
* Add source folders to monitor. Cannot be called after the watcher has been
@ -102,72 +107,143 @@ public class FileSystemWatcher {
*/
public void addSourceFolders(Iterable<File> folders) {
Assert.notNull(folders, "Folders must not be null");
synchronized (this.monitor) {
for (File folder : folders) {
addSourceFolder(folder);
}
}
}
/**
* Add a source folder to monitor. Cannot be called after the watcher has been
* {@link #start() started}.
* @param folder the folder to monitor
*/
public synchronized void addSourceFolder(File folder) {
public void addSourceFolder(File folder) {
Assert.notNull(folder, "Folder must not be null");
Assert.isTrue(folder.isDirectory(),
"Folder '" + folder + "' must exist and must" + " be a directory");
synchronized (this.monitor) {
checkNotStarted();
this.folders.put(folder, null);
}
}
/**
* Set an optional {@link FileFilter} used to limit the files that trigger a change.
* @param triggerFilter a trigger filter or null
*/
public synchronized void setTriggerFilter(FileFilter triggerFilter) {
public void setTriggerFilter(FileFilter triggerFilter) {
synchronized (this.monitor) {
this.triggerFilter = triggerFilter;
}
}
private void checkNotStarted() {
synchronized (this.monitor) {
Assert.state(this.watchThread == null, "FileSystemWatcher already started");
}
}
/**
* Start monitoring the source folder for changes.
*/
public synchronized void start() {
public void start() {
synchronized (this.monitor) {
saveInitialSnapshots();
if (this.watchThread == null) {
this.watchThread = new Thread() {
Map<File, FolderSnapshot> localFolders = new HashMap<File, FolderSnapshot>();
localFolders.putAll(this.folders);
this.watchThread = new Thread(new Watcher(this.remainingScans,
new ArrayList<FileChangeListener>(this.listeners),
this.triggerFilter, this.pollInterval, this.quietPeriod,
localFolders));
this.watchThread.setName("File Watcher");
this.watchThread.setDaemon(this.daemon);
this.watchThread.start();
}
}
}
private void saveInitialSnapshots() {
for (File folder : this.folders.keySet()) {
this.folders.put(folder, new FolderSnapshot(folder));
}
}
/**
* Stop monitoring the source folders.
*/
public void stop() {
stopAfter(0);
}
/**
* Stop monitoring the source folders.
* @param remainingScans the number of remaining scans
*/
void stopAfter(int remainingScans) {
synchronized (this.monitor) {
Thread thread = this.watchThread;
if (thread != null) {
this.remainingScans.set(remainingScans);
if (remainingScans <= 0) {
thread.interrupt();
}
if (Thread.currentThread() != thread) {
try {
thread.join();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
this.watchThread = null;
}
}
}
private static final class Watcher implements Runnable {
private final AtomicInteger remainingScans;
private final List<FileChangeListener> listeners;
private final FileFilter triggerFilter;
private final long pollInterval;
private final long quietPeriod;
private Map<File, FolderSnapshot> folders;
private Watcher(AtomicInteger remainingScans, List<FileChangeListener> listeners,
FileFilter triggerFilter, long pollInterval, long quietPeriod,
Map<File, FolderSnapshot> folders) {
this.remainingScans = remainingScans;
this.listeners = listeners;
this.triggerFilter = triggerFilter;
this.pollInterval = pollInterval;
this.quietPeriod = quietPeriod;
this.folders = folders;
}
@Override
public void run() {
int remainingScans = FileSystemWatcher.this.remainingScans.get();
int remainingScans = this.remainingScans.get();
while (remainingScans > 0 || remainingScans == -1) {
try {
if (remainingScans > 0) {
FileSystemWatcher.this.remainingScans.decrementAndGet();
this.remainingScans.decrementAndGet();
}
scan();
}
catch (InterruptedException ex) {
// Ignore
}
remainingScans = FileSystemWatcher.this.remainingScans.get();
remainingScans = this.remainingScans.get();
}
};
};
this.watchThread.setName("File Watcher");
this.watchThread.setDaemon(this.daemon);
this.remainingScans = new AtomicInteger(-1);
this.watchThread.start();
}
}
private void saveInitialSnapshots() {
for (File folder : this.folders.keySet()) {
this.folders.put(folder, new FolderSnapshot(folder));
}
}
private void scan() throws InterruptedException {
Thread.sleep(this.pollInterval - this.quietPeriod);
@ -231,34 +307,6 @@ public class FileSystemWatcher {
}
}
/**
* Stop monitoring the source folders.
*/
public synchronized void stop() {
stopAfter(0);
}
/**
* Stop monitoring the source folders.
* @param remainingScans the number of remaining scans
*/
synchronized void stopAfter(int remainingScans) {
Thread thread = this.watchThread;
if (thread != null) {
this.remainingScans.set(remainingScans);
if (remainingScans <= 0) {
thread.interrupt();
}
if (Thread.currentThread() != thread) {
try {
thread.join();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
this.watchThread = null;
}
}
}

Loading…
Cancel
Save