Reputation: 1351
So, I'm having a problem with a Gui i'm designing for a java app that renames all the files in a given directory to junk (Just for fun). This is the main block of code behind it all:
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Scanner;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
/**
* Class for renaming files to garbage names.
* All methods are static, hence private constructor.
* @author The Shadow Hacker
*/
public class RenameFiles {
private static int renamedFiles = 0;
private static int renamedFolders = 0;
public static char theChar = '#';
public static ArrayList<File> fileWhitelist = new ArrayList<>();
public static HashMap<File, File> revert = new HashMap<>();
public static int getRenamedFiles() {
return renamedFiles;
}
public static int getRenamedFolders() {
return renamedFolders;
}
/**
* All methods are static, hence private constructor.
*/
private RenameFiles() {
// Private constructor, nothing to do.
}
/**
* @param file The file to rename.
* @param renameTo The current value of the name to rename it to.
* @return A new value for renameTo.
*/
private static String renameFile(File file, String renameTo) {
for (File whitelistedFile : fileWhitelist) {
if (whitelistedFile.getAbsolutePath().equals(file.getAbsolutePath())) {
return renameTo;
}
}
if (new File(file.getParentFile().getAbsolutePath() + "/" + renameTo).exists()) {
renameTo += theChar;
renameFile(file, renameTo);
} else {
revert.put(new File(file.getParent() + "/" + renameTo), file);
file.renameTo(new File(file.getParent() + "/" + renameTo));
if (new File(file.getParent() + "/" + renameTo).isDirectory()) {
renamedFolders++;
} else {
renamedFiles++;
}
}
return renameTo;
}
/**
* TODO Add exception handling.
* @param dir The root directory.
* @throws NullPointerException if it can't open the dir
*/
public static void renameAllFiles(File dir) {
String hashtags = Character.toString(theChar);
for (File file : dir.listFiles()) {
if (file.isDirectory()) {
renameAllFiles(file);
hashtags = renameFile(file, hashtags);
} else {
hashtags = renameFile(file, hashtags);
}
}
}
public static void renameAllFiles(String dir) {
renameAllFiles(new File(dir));
}
/**
* This uses the revert HashMap to change the files back to their orignal names,
* if the user decides he didn't want to change the names of the files later.
* @param dir The directory in which to search.
*/
public static void revert(File dir) {
for (File file : dir.listFiles()) {
if (file.isDirectory()) {
revert(file);
}
revert.forEach((name, renameTo) -> {
if (file.getName().equals(name.getName())) {
file.renameTo(renameTo);
}
});
}
}
public static void revert(String dir) {
revert(new File(dir));
}
/**
* Saves the revert configs to a JSON file; can't use obj.writeJSONString(out)
* because a File's toString() method just calls getName(), and we want full
* paths.
* @param whereToSave The file to save the config to.
* @throws IOException
*/
@SuppressWarnings("unchecked")
public static void saveRevertConfigs(String whereToSave) throws IOException {
PrintWriter out = new PrintWriter(whereToSave);
JSONObject obj = new JSONObject();
revert.forEach((k, v) -> {
obj.put(k.getAbsolutePath(), v.getAbsolutePath());
});
out.write(obj.toJSONString());
out.close();
}
/**
* Warning - clears revert.
* Can't use obj.putAll(revert) because that puts the strings
* into revert, and we want Files.
* TODO Add exception handling.
* @param whereToLoad The path to the file to load.
* @throws ParseException If the file can't be read.
*/
@SuppressWarnings("unchecked")
public static void loadRevertConfigs(String whereToLoad) throws ParseException {
revert.clear();
((JSONObject) new JSONParser().parse(whereToLoad)).forEach((k, v) -> {
revert.put(new File((String) k), new File((String) v));
});
}
/**
* This static block is here because the program uses forEach
* loops, and we don't want the methods that call them to
* return errors.
*/
static {
if (!(System.getProperty("java.version").startsWith("1.8") || System.getProperty("java.version").startsWith("1.9"))) {
System.err.println("Must use java version 1.8 or above.");
System.exit(1);
}
}
/**
* Even though I made a gui for this, it still has a complete command-line interface
* because Reasons.
* @param argv[0] The folder to rename files in; defaults to the current directory.
* @throws IOException
*/
public static void main(String[] argv) throws IOException {
Scanner scanner = new Scanner(System.in);
String accept;
if (argv.length == 0) {
System.out.print("Are you sure you want to proceed? This could potentially damage your system! (y/n) : ");
accept = scanner.nextLine();
scanner.close();
if (!(accept.equalsIgnoreCase("y") || accept.equalsIgnoreCase("yes"))) {
System.exit(1);
}
renameAllFiles(System.getProperty("user.dir"));
} else if (argv.length == 1 && new File(argv[0]).exists()) {
System.out.print("Are you sure you want to proceed? This could potentially damage your system! (y/n) : ");
accept = scanner.nextLine();
scanner.close();
if (!(accept.equalsIgnoreCase("y") || accept.equalsIgnoreCase("yes"))) {
System.exit(1);
}
renameAllFiles(argv[0]);
} else {
System.out.println("Usage: renameAllFiles [\033[3mpath\033[0m]");
scanner.close();
System.exit(1);
}
System.out.println("Renamed " + (renamedFiles != 0 ? renamedFiles : "no") + " file" + (renamedFiles == 1 ? "" : "s")
+ " and " + (renamedFolders != 0 ? renamedFolders : "no") + " folder" + (renamedFolders == 1 ? "." : "s."));
}
}
As you can see, all of it's methods are static. Now here is my (Only partially completed) event handler class:
import java.io.File;
/**
* Seperate class for the gui event handlers.
* Mostly just calls methods from RenameFiles.
* Like RenameFiles, all methods are static.
* @author The Shadow Hacker
*/
public class EventHandlers {
private static Thread t;
/**
* The reason this is in a new thread is so we can check
* if it is done or not (For the 'cancel' option).
* @param dir The root directory used by RenameFiles.renameAllFiles.
*/
public static void start(File dir) {
t = new Thread(() -> {
RenameFiles.renameAllFiles(dir);
});
t.start();
}
/**
* @param dir The root directory used by RenameFiles.revert(dir).
* @throws InterruptedException
*/
public static void cancel(File dir) throws InterruptedException {
new Thread(() -> {
while (t.isAlive()) {
// Nothing to do; simply waiting for t to end.
}
RenameFiles.revert(dir);
}).start();
}
public static void main(String[] args) throws InterruptedException {
start(new File("rename"));
cancel(new File("rename"));
}
}
The problem I'm having is that when I run revert
from the RenameFiles class it works fine, but while running it from the multithreaded (We don't want the handlers to have to wait for the method to finish before reacting to another button press) EventHandlers class, revert dosn't work. Does this have something to do with RenameFiles being a class with all static methods, or something else? Please help!
Edit: @Douglas, when I run:
import java.io.File;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Seperate class for the gui event handlers.
* Mostly just calls methods from RenameFiles.
* Like RenameFiles, all methods are static.
* @author The Shadow Hacker
*/
public class EventHandlers {
private static ExecutorService service = Executors.newSingleThreadExecutor();
private static volatile CountDownLatch latch;
/**
* The reason this is in a new thread is so we can check
* if it is done or not (For the 'cancel' option).
* @param dir The root directory used by RenameFiles.renameAllFiles.
*/
public static void start(File dir) {
latch = new CountDownLatch(1);
service.submit(() -> {
RenameFiles.renameAllFiles(dir);
latch.countDown();
});
}
/**
* @param dir The root directory used by RenameFiles.revert(dir).
* @throws InterruptedException
*/
public static void cancel(File dir) throws InterruptedException {
service.submit(() -> {
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
RenameFiles.revert(dir);
});
}
The program just runs forever, without terminating.
Upvotes: 0
Views: 633
Reputation: 5087
You have two major problems here.
First, you are sharing variables between threads. Default variable handling in Java has no guarantee that two threads will agree on what value any given variable has. You can fix this one by giving each variable the volatile
modifier (note: this can decrease performance, which is why it's not default).
Second, you have no mechanism in place to guarantee anything about thread execution order. As written, it is entirely possible for EventHandlers.main
to run cancel
to completion before the renameAllFiles
call even starts. It is also possible for the renaming to start, get paused by the thread scheduler, cancel run from beginning to end, and then renaming finish, or any of a bunch of other combinations. You attempted to do something about this with the t.isAlive()
check, but your redundant creation of yet another Thread
in main
means there's no guarantee t
is even initialized before the main thread gets there. It would be an unlikely but valid by the spec possibility for you to get a NullPointerException
from that line.
This second problem is a much harder one to fix in general, and is the primary reason working with threads is infamously difficult. Fortunately this particular problem is a fairly simple case. Instead of looping forever on the isAlive()
check, create a CountDownLatch
when you start the thread, count it down when the thread finishes, and simply await()
it in cancel
. This will incidentally also solve the first problem at the same time without any need for volatile
, because in addition to its scheduling coordination a CountDownLatch
guarantees that any thread that awaited on it will see the results of everything done in any thread that counted it down.
So, long story short, steps to fix this:
new Thread
in main
and just call start
directly. start
creates a Thread
itself, there's no need to nest that inside another Thread
.Thread t
with a CountDownLatch
.start
, initialize the CountDownLatch
with a count of 1.start
, after initializing the CountDownLatch
, get an ExecutorService
by calling Executors.newSingleThreadExecutor()
, and then submit
the renameAllFiles
call to it. Do this instead of using a Thread
directly. Among other things, the specification guarantees that anything done before that will be visible as expected in the new thread, and I don't see any such guarantee in the documentation of Thread.start()
. It's also got a lot more convenience and utility methods.ExecutorService
, after the renaming, call countDown()
on the latch.submit
, call shutdown()
on the ExecutorService
. This will prevent you from reusing the same one, but stops it from waiting indefinitely for reuse that will never happen.cancel
, replace the while
loop with a call to await()
on the latch. In addition to the memory consistency guarantee, this will improve performance by letting the system thread scheduler handle the wait instead of spending CPU time on looping.Additional changes will be needed if you want to account for multiple rename operations in the same run of the program.
Upvotes: 2