简体   繁体   中英

Java program with multiple threads not working

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.

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:

  1. Remove the new Thread in main and just call start directly. start creates a Thread itself, there's no need to nest that inside another Thread .
  2. Replace the Thread t with a CountDownLatch .
  3. In start , initialize the CountDownLatch with a count of 1.
  4. In 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.
  5. Inside what you submit to the ExecutorService , after the renaming, call countDown() on the latch.
  6. After the 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.
  7. In 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM