簡體   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