繁体   English   中英

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

[英]Java program with multiple threads not working

因此,我在使用Gui时遇到了问题,我正在设计一个Java应用程序,该应用程序将给定目录中的所有文件重命名为垃圾文件(只是为了好玩)。 这是所有内容的主要代码块:

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

如您所见,它的所有方法都是静态的。 现在这是我的(仅部分完成的)事件处理程序类:

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类运行revert ,它工作正常,但是在多线程环境中运行时(我们不希望处理程序在对另一个按钮按下做出反应之前必须等待方法完成) )EventHandlers类,无法恢复。 这与RenameFiles是具有所有静态方法的类有关吗,还是其他原因? 请帮忙!

编辑:@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);
        });
    }

该程序将永远运行,而不会终止。

您在这里有两个主要问题。

首先,您在线程之间共享变量。 Java中的默认变量处理不能保证两个线程将就给定变量具有什么值达成共识。 您可以通过为每个变量赋予volatile修饰符来解决此问题(注意:这会降低性能,这就是为什么它不是默认值)。

其次,您没有任何机制可以保证有关线程执行顺序的任何信息。 如所写, EventHandlers.main完全有可能在renameAllFiles调用甚至开始之前运行cancel以完成操作。 重命名也可能开始,被线程调度程序暂停,从头到尾取消运行,然后重命名完成或其他任何组合。 您试图通过t.isAlive()检查对此做一些事情,但是在main冗余创建另一个Thread意味着甚至不能保证t在主线程到达那里之前就被初始化了。 从规范中获得NullPointerException可能性不大,但仍然有效。

第二个问题通常很难解决,这是使用线程非常困难的主要原因。 幸运的是,这个特定问题是一个非常简单的案例。 与其在isAlive()检查中永远循环, CountDownLatch在启动线程时创建一个CountDownLatch ,在线程完成时倒数,并在cancel简单地await() 顺便说一句,这也将同时解决第一个问题,而不需要volatile ,因为除了其调度协调之外, CountDownLatch保证了等待的线程将在将其CountDownLatch任何线程中看到所做的一切结果。

因此,长话短说,解决此问题的步骤:

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

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

暂无
暂无

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

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