简体   繁体   English

具有多个线程的Java程序不起作用

[英]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). 因此,我在使用Gui时遇到了问题,我正在设计一个Java应用程序,该应用程序将给定目录中的所有文件重命名为垃圾文件(只是为了好玩)。 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. 我遇到的问题是,当我从RenameFiles类运行revert ,它工作正常,但是在多线程环境中运行时(我们不希望处理程序在对另一个按钮按下做出反应之前必须等待方法完成) )EventHandlers类,无法恢复。 Does this have something to do with RenameFiles being a class with all static methods, or something else? 这与RenameFiles是具有所有静态方法的类有关吗,还是其他原因? Please help! 请帮忙!

Edit: @Douglas, when I run: 编辑:@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);
        });
    }

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. Java中的默认变量处理不能保证两个线程将就给定变量具有什么值达成共识。 You can fix this one by giving each variable the volatile modifier (note: this can decrease performance, which is why it's not default). 您可以通过为每个变量赋予volatile修饰符来解决此问题(注意:这会降低性能,这就是为什么它不是默认值)。

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. 如所写, EventHandlers.main完全有可能在renameAllFiles调用甚至开始之前运行cancel以完成操作。 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. 您试图通过t.isAlive()检查对此做一些事情,但是在main冗余创建另一个Thread意味着甚至不能保证t在主线程到达那里之前就被初始化了。 It would be an unlikely but valid by the spec possibility for you to get a NullPointerException from that line. 从规范中获得NullPointerException可能性不大,但仍然有效。

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 . 与其在isAlive()检查中永远循环, CountDownLatch在启动线程时创建一个CountDownLatch ,在线程完成时倒数,并在cancel简单地await() 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. 顺便说一句,这也将同时解决第一个问题,而不需要volatile ,因为除了其调度协调之外, CountDownLatch保证了等待的线程将在将其CountDownLatch任何线程中看到所做的一切结果。

So, long story short, steps to fix this: 因此,长话短说,解决此问题的步骤:

  1. Remove the new Thread in main and just call start directly. 删除mainnew Thread ,然后直接调用start start creates a Thread itself, there's no need to nest that inside another Thread . start本身会创建一个Thread ,而无需将其嵌套在另一个Thread
  2. Replace the Thread t with a CountDownLatch . CountDownLatch替换Thread t
  3. In start , initialize the CountDownLatch with a count of 1. start ,使用1计数初始化CountDownLatch
  4. In start , after initializing the CountDownLatch , get an ExecutorService by calling Executors.newSingleThreadExecutor() , and then submit the renameAllFiles call to it. start初始化CountDownLatch ,通过调用Executors.newSingleThreadExecutor()获得ExecutorService ,然后submit renameAllFiles调用。 Do this instead of using a Thread directly. 这样做,而不是直接使用Thread 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() . 除其他事项外,该规范保证在此之前所做的任何操作都将在新线程中按预期显示,并且在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. 重命名后,在提交给ExecutorService内容内部,调用闩锁上的countDown()
  6. After the submit , call shutdown() on the ExecutorService . submit ,在ExecutorService上调用shutdown() 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. cancel ,将while循环替换为对闩锁上的await()的调用。 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. 除了保证内存一致性之外,这还可以通过让系统线程调度程序处理等待而不是在循环上花费CPU时间来提高性能。

Additional changes will be needed if you want to account for multiple rename operations in the same run of the program. 如果要在程序的同一运行中考虑多个重命名操作,则需要进行其他更改。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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