Java 多线程程序不工作

Java program with multiple threads not working

所以,我在为 java 应用程序设计的 Gui 遇到问题,该应用程序将给定目录中的所有文件重命名为垃圾文件(只是为了好玩)。这是其背后的主要代码块:

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 [3[3mpath3[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."));
    }
}

如您所见,它的所有方法都是静态的。现在这是我的(仅部分完成的)事件处理程序 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"));
    }
}

我遇到的问题是,当我从 RenameFiles class 运行 revert 时它工作正常,但是当 运行 从多线程 (我们不希望处理程序在对另一个按钮按下做出反应之前必须等待方法完成)EventHandlers class,恢复不起作用。这与 RenameFiles 是所有静态方法的 class 或其他什么有关吗?请帮忙!

编辑:@Douglas,当我 运行:

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);
        });
    }

程序永远 运行s,不会终止。

这里有两个主要问题。

首先,您在线程之间共享变量。 Java 中的默认变量处理不保证两个线程会就任何给定变量的值达成一致。您可以通过为每个变量提供 volatile 修饰符来解决这个问题(注意:这会降低性能,这就是它不是默认值的原因)。

其次,您没有适当的机制来保证有关线程执行顺序的任何事情。如所写,EventHandlers.main 到 运行 cancel 完全有可能在 renameAllFiles 调用甚至开始之前完成。重命名也有可能开始,被线程调度程序暂停,从头到尾取消 运行,然后重命名完成,或任何其他组合。你试图用 t.isAlive() 检查来解决这个问题,但是你在 main 中冗余创建另一个 Thread 意味着不能保证 t 在主线程之前甚至被初始化到达那里。从该行获得 NullPointerException 的规范可能性不太可能但有效。

第二个问题通常更难解决,这也是使用线程非常困难的主要原因。幸运的是,这个特殊问题是一个相当简单的案例。不要在 isAlive() 检查上永远循环,而是在启动线程时创建一个 CountDownLatch,在线程结束时对其进行倒计时,然后在 cancel 中简单地 await()。这将同时解决第一个问题,而不需要 volatile,因为除了它的调度协调之外,CountDownLatch 保证任何等待它的线程都会看到所有完成的结果在任何倒数的线程中。

所以,长话短说,解决这个问题的步骤:

  1. 去掉main中的new Thread,直接调用start即可。 start 本身会创建一个 Thread,无需将其嵌套在另一个 Thread.
  2. Thread t 替换为 CountDownLatch
  3. start中,用计数1初始化CountDownLatch
  4. start中,初始化CountDownLatch之后,通过调用Executors.newSingleThreadExecutor()得到一个ExecutorService,然后submit 对它的 renameAllFiles 调用。这样做而不是直接使用 Thread。除其他事项外,该规范保证在此之前完成的任何操作都将在新线程中按预期可见,而我在 Thread.start() 的文档中没有看到任何此类保证。它还提供了更多方便实用的方法。
  5. 在你提交给ExecutorService的内容里面,重命名后,在latch上调用countDown()
  6. submit 之后,在 ExecutorService 上调用 shutdown()。这将防止您重复使用同一个,但会阻止它无限期地等待永远不会发生的重复使用。
  7. cancel 中,将 while 循环替换为对锁存器上 await() 的调用。除了内存一致性保证之外,这将通过让系统线程调度程序处理等待而不是花费 CPU 时间在循环上来提高性能。

如果您想在同一个 运行 程序中考虑多个重命名操作,则需要进行其他更改。