高效率读取大文件进行数据处理-通过扩展RandomAccessFile类使之具备Buffer改善I/O性能

主体:

目前最流行的J2SDK版本是1.3系列。使用该版本的开发人员需文件随机存取,就得使用RandomAccessFile类。其I/O性能较之其它常用开发语言的同类性能差距甚远,严重影响程序的运行效率。

开发人员迫切需要提高效率,下面分析RandomAccessFile等文件类的源代码,找出其中的症结所在,并加以改进优化,创建一个"性/价比"俱佳的随机文件访问类BufferedRandomAccessFile。

在改进之前先做一个基本测试:逐字节COPY一个12兆的文件(这里牵涉到读和写)。

我们可以看到两者差距约32倍,RandomAccessFile也太慢了。先看看两者关键部分的源代码,对比分析,找出原因。

1.1.[RandomAccessFile]

public class RandomAccessFile implements DataOutput, DataInput {
    public final byte readByte() throws IOException {
        int ch = this.read();
        if (ch < 0)
            throw new EOFException();
        return (byte)(ch);
    }
    public native int read() throws IOException; 
    public final void writeByte(int v) throws IOException {
        write(v);
    }
    public native void write(int b) throws IOException; 
}


可见,RandomAccessFile每读/写一个字节就需对磁盘进行一次I/O操作。

1.2.[BufferedInputStream]

public class BufferedInputStream extends FilterInputStream {
    private static int defaultBufferSize = 2048; 
    protected byte buf[]; // 建立读缓存区
    public BufferedInputStream(InputStream in, int size) {
        super(in);       
        if (size <= 0) {
            throw new IllegalArgumentException("Buffer size <= 0");
        }
        buf = new byte[size];
    }
    public synchronized int read() throws IOException {
        ensureOpen();
        if (pos >= count) {
            fill();
            if (pos >= count)
                return -1;
        }
        return buf[pos++] & 0xff; // 直接从BUF[]中读取
    }
    private void fill() throws IOException {
    if (markpos < 0)
        pos= 0;        /* no mark: throw away the buffer */
    else if (pos >= buf.length)  /* no room left in buffer */
        if (markpos > 0) {   /* can throw away early part of the buffer */
        int sz = pos - markpos;
        System.arraycopy(buf, markpos, buf, 0, sz);
        pos = sz;
        markpos = 0;
        } else if (buf.length >= marklimit) {
        markpos = -1;   /* buffer got too big, invalidate mark */
        pos = 0;    /* drop buffer contents */
        } else {        /* grow buffer */
        int nsz = pos * 2;
        if (nsz > marklimit)
            nsz = marklimit;
        byte nbuf[] = new byte[nsz];
        System.arraycopy(buf, 0, nbuf, 0, pos);
        buf = nbuf;
        }
    count = pos;
    int n = in.read(buf, pos, buf.length - pos);
    if (n > 0)
        count = n + pos;
    }
}


1.3.[BufferedOutputStream]

public class BufferedOutputStream extends FilterOutputStream {
   protected byte buf[]; // 建立写缓存区
   public BufferedOutputStream(OutputStream out, int size) {
        super(out);
        if (size <= 0) {
            throw new IllegalArgumentException("Buffer size <= 0");
        }
        buf = new byte[size];
    }
public synchronized void write(int b) throws IOException {
        if (count >= buf.length) {
            flushBuffer();
        }
        buf[count++] = (byte)b; // 直接从BUF[]中读取
   }
   private void flushBuffer() throws IOException {
        if (count > 0) {
            out.write(buf, 0, count);
            count = 0;
        }
   }
}


可见,Buffered I/O putStream每读/写一个字节,若要操作的数据在BUF中,就直接对内存的buf[]进行读/写操作;否则从磁盘相应位置填充buf[],再直接对内存的buf[]进行读/写操作,绝大部分的读/写操作是对内存buf[]的操作。

1.3.小结

内存存取时间单位是纳秒级(10E-9),磁盘存取时间单位是毫秒级(10E-3), 同样操作一次的开销,内存比磁盘快了百万倍。理论上可以预见,即使对内存操作上万次,花费的时间也远少对于磁盘一次I/O的开销。 显然后者是通过增加位于内存的BUF存取,减少磁盘I/O的开销,提高存取效率的,当然这样也增加了BUF控制部分的开销。从实际应用来看,存取效率提高了32倍。

根据1.3得出的结论,现试着对RandomAccessFile类也加上缓冲读写机制。

随机访问类与顺序类不同,前者是通过实现DataInput/DataOutput接口创建的,而后者是扩展FilterInputStream/FilterOutputStream创建的,不能直接照搬。

2.1.开辟缓冲区BUF[默认:1024字节],用作读/写的共用缓冲区。

<think>嗯,用户问的是Java中如何高效读取50GB级别的大文件,还要支持多线程处理,基于JDK1.8。首先,我得考虑大文件处理的一般方法。传统的方式比如用BufferedReader逐行读取可能不太适合,因为文件太大,内存会爆掉。所以需要分块读取,或者使用内存映射文件。然后用户提到多线程,所以需要把文件分成多个部分,让不同的线程处理不同的块。但怎么分块呢?比如按行分的话可能比较麻烦,因为行的长度不一,无法直接计算偏移量。所以可能需要按字节分块,然后在每个块的开始结束位置调整,确保不截断行。 接下来想到Java中的FileChannelMappedByteBuffer,内存映射文件可以将文件直接映射到内存,这样读写速度快,尤其是对大文件。然后结合多线程,每个线程处理不同的文件区域。 还要注意线程之间的分块边界处理,比如每个线程处理一个块,但块的起始位置可能在上一个行的中间,所以需要找到下一个换行符的位置作为起始,同样结束位置也要找到下一个换行符,避免断行。 另外,使用ExecutorService来管理线程池,提高效率。分块的数量可能CPU核心数相关,比如Runtime.getRuntime().availableProcessors()获取可用的处理器数量,然后创建相应数量的线程。 代码方面,需要实现Runnable或Callable的任务,每个任务处理一个文件块。使用FileChannel.map()方法创建MappedByteBuffer,然后每个线程处理自己的buffer区域。 可能遇到的问题包括文件的分块不均匀,或者某些块处理时间过长,这时候需要考虑动态分配任务,或者确保分块足够小。但50G的文件分块的话,比如每个块几百MB,可能比较合适。 还需要考虑字符编码的问题,比如文件是否是UTF-8,处理的时候需要正确解码。不过用户的问题中没有特别提到编码,可能默认使用系统编码,但最好明确指定,比如StandardCharsets.UTF_8。 另外,处理过程中是否允许修改文件?如果只是读取的话,内存映射文件应该是只读模式,这样更安全。最后,测试分块是否正确,确保没有遗漏行或者重复处理。可能需要一些测试用例来验证分块的正确性,比如在分块边界处是否有换行符,或者如何处理没有换行符的情况。 综上,步骤应该是: 1. 使用FileChannel打开文件,获取总大小。2. 根据线程数计算每个线程处理的块大小,比如总大小除以线程数。3. 为每个线程分配起始结束位置,调整起始位置到下一行的开始,结束位置到行尾。 4.每个线程使用MappedByteBuffer处理自己的块,逐行读取处理。 5.使用线程池管理多线程任务,提高效率。</think>对于50GB级别大文件的高效读取,推荐使用内存映射文件(MappedByteBuffer)结合多线程分块处理。以下是基于JDK1.8的实现方案: ```java import java.io.RandomAccessFile; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; import java.nio.charset.StandardCharsets; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class BigFileReader { private static final int THREAD_COUNT = Runtime.getRuntime().availableProcessors(); private static final long SEGMENT_SIZE = 256 * 1024 * 1024; // 256MB per segment public static void main(String[] args) throws Exception { String filePath = "large_file.txt"; try (RandomAccessFile file = new RandomAccessFile(filePath, "r"); FileChannel channel = file.getChannel()) { long fileSize = channel.size(); ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT); long startPosition = 0; while (startPosition < fileSize) { long endPosition = Math.min(startPosition + SEGMENT_SIZE, fileSize); endPosition = findLineEnd(channel, endPosition, fileSize); if (endPosition == -1) endPosition = fileSize; final long finalStart = startPosition; final long finalEnd = endPosition; executor.submit(() -> processSegment(channel, finalStart, finalEnd)); startPosition = endPosition; } executor.shutdown(); } } private static long findLineEnd(FileChannel channel, long endPosition, long fileSize) { try { MappedByteBuffer buffer = channel.map( FileChannel.MapMode.READ_ONLY, Math.max(endPosition - 1024, 0), Math.min(1024, fileSize - Math.max(endPosition - 1024, 0)) ); for (int i = buffer.limit() - 1; i >= 0; i--) { if (buffer.get(i) == '\n') { return Math.max(endPosition - 1024, 0) + i + 1; } } } catch (Exception e) { e.printStackTrace(); } return -1; } private static void processSegment(FileChannel channel, long start, long end) { try { MappedByteBuffer buffer = channel.map( FileChannel.MapMode.READ_ONLY, start, end - start ); byte[] bytes = new byte[(int)(end - start)]; buffer.get(bytes); String segment = new String(bytes, StandardCharsets.UTF_8); // 处理分段内容 processContent(segment); } catch (Exception e) { e.printStackTrace(); } } private static void processContent(String content) { // 实际业务处理逻辑 String[] lines = content.split("\n"); for (String line : lines) { // 处理每一行数据 } } } ``` 代码说明: 1. **内存映射技术**:使用`MappedByteBuffer`将文件直接映射到内存,绕过JVM堆内存限制 2. **分块策略**: - 每个分块256MB(可根据硬件调整) - 使用`findLineEnd()`确保分块在完整行结尾处 3. **多线程处理**: - 使用固定大小线程池(CPU核心数) - 每个线程处理独立文件块 4. **边界处理**: - 在分块边界处向前查找换行符 - 确保不会截断行数据
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值