如何使用java来实现多线程的读写文件操作

现在想用java实现以下功能:
将某目录下的文件读取出来,经过业务处理之后,再按照一定条件写成新文件。
目前我的思路是这样的
1、要可以配置读的线程数,每个线程读一个文件,然后将读取到的结果放到多个队列(也不一定要多个队列,只希望高效一些)中
2、要可以配置写线程数,每个线程写一个文件,从队列中取。
3、要保证异常机制,也就是程序挂了,下次启动的时候能够继续处理

其中这边的多个线程如何协同读写,已经程序在某个时刻挂掉了,然后重启的时候如何能恢复之前的状态,这一块我还不会设计,有什么好的设计思路吗?

1、多线程要考虑并发读写同一个文件的问题。既然这样,可以主线程列出待读写的文件列表W,然后使用线程安全的方式如Collections.synchronizedList(list),线程池里每个线程领取一个任务(文件),并且将这个文件移入正在处理的文件列表P中(如CopyOnWriteArrayList,这个是写的时候加锁),按要求处理完,再将这个文件从正在处理的文件列表,移到已处理完的文件列表中。再获取下一个文件进行处理,直到列表中所有文件都处理完。
2、可以在每次文件列表有变化时、或者定时(如每n秒)、或者在暂停处理时,将上述待处理文件列表和正在处理的文件列表序列化以后写入磁盘文件S。如果程序中断,重新启动程序时,主线程除了前面提到的工作,增加一步读取该磁盘文件S,如果该文件存在,反序列化两个文件列表的内容,以这两个列表为准。等待所有线程处理完以后,再清理掉这个磁盘文件。
3、考虑程序中断后继续处理,待处理的文件可能有变化,与磁盘文件S中存的列表有差异,因此各线程处理文件时,要考虑文件实际不存在的异常情况。主线程要在列出待读写的文件列表P以后,更新磁盘文件S中反序列化得到的列表。
线程池关键部分代码如下:

        // 创建n个线程
        int n = 10;
        ThreadPoolExecutor executor = new ThreadPoolExecutor(n, n, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
        for (int i = 0; i < n; i++) {
            executor.execute(() -> {
                  // 每个线程要做的事情
                  // 领取任务、读写文件等
            });
        }
        //关闭线程池
        executor.shutdown();
        //等待线程池中所有线程执行完毕
        try {
            executor.awaitTermination(1, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

纯人工想法,其它部分如有问题,可以和我私信交流

要保证异常机制,也就是程序挂了,下次启动的时候能够继续处理
这个可以用数据库记录那个文件没有处理完全,搞个补偿机制

回答申明:包含AI辅助答案参考ChatGPT Plus版
实现多线程协同读写以及恢复之前的状态可以使用 Java 的线程池和持久化技术来完成。下面是一个基本的代码示例,展示了如何实现目录文件的读取、处理和写入,并提供了一种简单的状态恢复机制。

import java.io.*;
import java.util.concurrent.*;

class FileProcessor implements Runnable {
    private BlockingQueue<File> queue;
    private String outputDirectory;

    public FileProcessor(BlockingQueue<File> queue, String outputDirectory) {
        this.queue = queue;
        this.outputDirectory = outputDirectory;
    }

    @Override
    public void run() {
        try {
            while (true) {
                File file = queue.take(); // 从队列中取出文件进行处理
                // TODO: 根据业务逻辑处理文件
                // 在这里可以对文件进行读取、处理和写入操作

                // 保存处理结果到新文件
                String newFileName = outputDirectory + File.separator + "new_" + file.getName();
                File newFile = new File(newFileName);
                // TODO: 将处理结果写入新文件

                System.out.println("Processed file: " + file.getName());
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

class FileHandler {
    private static final String OUTPUT_DIR = "output";
    private static final int NUM_READ_THREADS = 5;
    private static final int NUM_WRITE_THREADS = 3;
    private static final String STATE_FILE = "state.ser";

    private BlockingQueue<File> fileQueue;
    private ExecutorService readExecutor;
    private ExecutorService writeExecutor;

    public FileHandler() {
        fileQueue = new LinkedBlockingQueue<>();
        readExecutor = Executors.newFixedThreadPool(NUM_READ_THREADS);
        writeExecutor = Executors.newFixedThreadPool(NUM_WRITE_THREADS);
    }

    public void startProcessing(String directory) {
        // 读取目录下的所有文件并添加到队列
        File[] files = new File(directory).listFiles();
        for (File file : files) {
            fileQueue.offer(file);
        }

        // 创建写线程
        for (int i = 0; i < NUM_WRITE_THREADS; i++) {
            writeExecutor.submit(new FileProcessor(fileQueue, OUTPUT_DIR));
        }
    }

    public void saveState() {
        try (ObjectOutputStream outputStream = new ObjectOutputStream(
                new FileOutputStream(STATE_FILE))) {
            outputStream.writeObject(fileQueue);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void restoreState() {
        try (ObjectInputStream inputStream = new ObjectInputStream(
                new FileInputStream(STATE_FILE))) {
            fileQueue = (BlockingQueue<File>) inputStream.readObject();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    public void stopProcessing() {
        readExecutor.shutdown();
        writeExecutor.shutdown();
    }
}

public class Main {
    public static void main(String[] args) {
        FileHandler fileHandler = new FileHandler();
        fileHandler.restoreState(); // 恢复之前的状态

        // 处理文件
        fileHandler.startProcessing("input_directory");

        // 做一些其他的工作

        // 保存当前状态
        fileHandler.saveState();

        // 停止处理
        fileHandler.stopProcessing();
    }
}

上述代码示例中,通过 FileHandler 类实现了文件处理的功能。在 startProcessing 方法中

,读取指定目录下的所有文件,并将文件添加到阻塞队列 fileQueue 中。然后,使用固定数量的写线程从队列中取出文件进行处理和写入操作。

通过调用 saveState 方法,可以将当前的文件处理状态保存到文件中。在下次启动程序时,可以使用 restoreState 方法从文件中恢复之前的状态。

需要注意的是,代码示例中只是提供了一个简单的框架,你可以根据自己的实际需求进行适当的修改和扩展。此外,对于处理文件异常或程序挂掉的情况,还需要进一步考虑如何处理未处理的文件和状态的一致性等问题。

设计思路:

首先,您可以使用Java的多线程机制来并发读取文件,每个线程读取一个文件,并将处理结果放入一个共享的阻塞队列中。可以使用线程池来管理线程,并限制线程数。

接下来,您可以使用另外一组线程来并发写文件,从共享的阻塞队列中获取文件处理结果,并将其写入新文件中。同样,可以使用线程池来管理线程,并限制线程数。

为了保证异常机制,您可以在读取和写入文件时,使用try-catch语句来捕获异常,并将异常信息存储到一个日志文件中。如果程序挂掉了,可以在重启后读取日志文件,并根据日志文件中的信息恢复之前的状态。

如果您希望程序在挂掉后能够自动恢复之前的状态,您可以考虑将文件处理结果保存到一个永久性存储介质中,例如数据库、文件或消息队列。在程序重启后,可以读取永久性存储介质中的数据,并将其放入共享的阻塞队列中,然后再启动读取和写入文件的线程。这样可以确保程序在挂掉后能够自动恢复之前的状态。

另外,为了避免重复处理已经处理过的文件,您可以在处理每个文件时,将其文件名存储到一个已处理文件名的集合中。在读取文件时,可以先检查文件名是否已经存在于已处理文件名的集合中,如果是则跳过该文件。这样可以避免重复处理已经处理过的文件。

最后,为了确保程序的可靠性和稳定性,建议您在编写程序时,使用一些成熟的Java框架和库,例如Spring、Quartz、Apache Commons等,来简化开发和管理。此外,还可以使用一些Java监控工具,例如JConsole、VisualVM、JProfiler等,来帮助您诊断和调试程序问题。

在多线程协同读写时,需要使用同步机制来确保数据的一致性和正确性。常用的同步机制包括互斥锁、信号量、条件变量等。

具体来说,可以使用互斥锁来保证同一时间只有一个线程可以访问共享资源,使用信号量来控制多个线程对共享资源的访问,使用条件变量来协调不同线程之间的执行顺序。

当程序在某个时刻挂掉后,可以使用事务性内存(Transactional Memory)来保证状态的一致性和完整性。事务性内存是一种将多个操作打包成一个原子操作的机制,可以保证这些操作要么全部执行成功,要么全部不执行,从而避免了并发访问带来的数据不一致问题。在重启程序后,可以使用日志(Log)来恢复之前的状态。具体来说,将程序状态的变化记录到日志中,在程序重启后可以根据日志来恢复之前的状态。

另外,还可以使用分布式系统架构来解决这个问题。在分布式系统中,不同的进程或线程运行在不同的机器上,通过远程过程调用(RPC)或消息队列(Message Queue)等方式进行通信和协作,从而避免了多线程协同读写带来的问题。在分布式系统中,可以使用数据库或其他存储系统来管理状态,确保状态的一致性和完整性。

综上所述,可以使用互斥锁、信号量、条件变量、事务性内存、日志和分布式系统来解决多线程协同读写和状态恢复的问题。具体实现方式需要根据具体的应用场景和需求进行选择和设计。

在项目中我经常使用Commons IO和Netty读写文件,不知道你能用的上不,你可以找一个FileUtils工具类带多线程操作的试试

读文件是很快的,你应该只用一个线程读,读到文件后交给work线程去处理并写出新文件


import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Scanner;
 public class FileReadWrite {
     public static void main(String[] args) {
         String fileName = "example.txt";
        File file = new File(fileName);
         // 启动一个线程进行文件的读操作
        new Thread(() -> {
            try {
                Scanner scanner = new Scanner(file);
                while (scanner.hasNextLine()) {
                    String line = scanner.nextLine();
                    System.out.println(line);
                }
                scanner.close();
            } catch (FileNotFoundException e) {
                System.out.println("文件不存在");
            }
        }).start();
         // 启动一个线程进行文件的写操作
        new Thread(() -> {
            try {
                FileWriter writer = new FileWriter(file, true);
                writer.write("Hello, World!\n");
                writer.close();
            } catch (IOException e) {
                System.out.println("文件写入失败");
            }
        }).start();
     }
 }

可以参考下

package List;
 
import javafx.concurrent.Worker;
 
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
 
/**
 * Description: 实现多线程对同一个文件的并行读写
 *
 *
 * @author Mayber
 * @date Created on 2022/4/10
 */
public class ParalFile {
 
    
 
    public AtomicInteger index = new AtomicInteger(0);
    public long size;
 
    public int buffer_size;
 
    RandomAccessFile orign_raf;
    RandomAccessFile target_raf;
    FileChannel orignFileChannel;
    FileChannel targetFileChannel;
 
    public ParalFile(File orign,int buffer_size) throws FileNotFoundException {
        RandomAccessFile orign_raf = new RandomAccessFile(orign,"rw");
        // RandomAccessFile target_raf = new RandomAccessFile(target,"rw");
        this.orignFileChannel = orign_raf.getChannel();
        this.targetFileChannel = target_raf.getChannel();
        this.buffer_size = buffer_size;
        this.size = orign.length();
        System.out.println("构造完毕");
    }
 
 
    class ReadTask implements Runnable{
        @Override
        public void run() {
            // 这个任务中需要使用cas获取到当前的 index,并且读取index+buffer值,然后将index改为
            int cur_index;
            System.out.println("执行");
            while((cur_index = index.get())<size){
 
                int target_index = (cur_index+buffer_size)>size? (int)size :cur_index+buffer_size;
 
 
                if(index.compareAndSet(cur_index,target_index+1)){
                    //如果cas 成功就进行读写操作
                 
                    System.out.println(Thread.currentThread().getName()+"读取了cur_index"+cur_index+"到"+target_index+"的缓冲数据");
                    byte[] content = readFile(cur_index,target_index);
 
                    // 读取到了内容可以在下面进行一些别的处理操作
 
                }
            }
 
        }
 
        public byte[] readFile(int orign_index,int target_index){
 
            // 读取文件,使用一个map内存映射进行读取,可以加速读取吧
            MappedByteBuffer map;
            byte[] byteArr = new byte[target_index-orign_index];
            try {
                map = orignFileChannel.map(FileChannel.MapMode.READ_ONLY,orign_index,target_index-orign_index);
 
                map.get(byteArr,0,target_index-orign_index);
            } catch (Exception e) {
                System.out.println("读取"+orign_index+"到"+target_index+"失败");
                e.printStackTrace();
            }
            return byteArr;
 
 
        }
 
 
    }
 
    class WriteTask implements Runnable{
 
        @Override
        public void run() {
 
            byte[] a = new byte[1024];
            int cur_index;
            System.out.println("执行");
            while((cur_index = index.get())<size){
 
                int target_index = (cur_index+buffer_size)>size? (int)size :cur_index+buffer_size;
 
 
                if(index.compareAndSet(cur_index,target_index+1)){
                    //如果cas 成功就进行读写操作
                    //成功
                    System.out.println(Thread.currentThread().getName()+"写了cur_index"+cur_index+"到"+target_index+"的缓冲数据");
                    writeFile(cur_index,target_index,a);
 
                    // 读取
 
                }
            }
        }
 
        public void writeFile(int orign_index,int target_index,byte[] content){
            //然后进行写
            // 读取文件,使用一个map内存映射进行读取,可以加速读取吧
            MappedByteBuffer map;
            byte[] byteArr = new byte[target_index-orign_index];
            try {
                map = targetFileChannel.map(FileChannel.MapMode.READ_ONLY,orign_index,target_index-orign_index);
 
                map.position();
                map.put(content);
            } catch (IOException e) {
                e.printStackTrace();
            }
 
 
        }
    }
 
    public static void main(String[] args) throws FileNotFoundException {
 
        File orign = new File("D:\\work_space\\project\\leetcode\\src\\test.txt");
        ParalFile pf = new ParalFile(orign,30);
        ThreadPoolExecutor poll = new ThreadPoolExecutor(4,5,1, TimeUnit.HOURS,new LinkedBlockingDeque<>(10) );
 
        for(int i=0;i<5;i++){
            poll.execute(pf.new ReadTask());
        }
 
 
 
    }
 
 
 
 
}