数组,transferFrom,ByteBuffer数据传输对比

本文对比了使用通道、数组和字节缓冲区三种方法进行大文件传输的性能,结果显示通道传输在不同文件大小下表现最佳。
package cache;


import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Random;


public class CacheTest {
public static void main(String args[]) throws IOException {


File inFile = new File("f:\\test\\CopyOfBlockCache.java");
File outFile = new File("f:\\test\\channel\\"
+ (new Random().nextInt()));


/*
 * for (int i = 0; i < 1000; i++) { getOutStream(file); }
 */


InputStream blockIn = getBlockInputStream(inFile);
FileChannel fileCh = ((FileInputStream) blockIn).getChannel();
if (!outFile.getParentFile().exists())
outFile.getParentFile().mkdirs();
OutputStream out = getOutStream(outFile);
long dataLength = 0;
long size = inFile.length();
long everLong = 4 * 1024 * 1024;
long tmp;
long start = System.currentTimeMillis();
while (dataLength < size) {
tmp = ((FileOutputStream) out).getChannel().transferFrom(fileCh,
dataLength, everLong);
dataLength += tmp;
// System.out.println("dataLength="+dataLength);
// System.out.println("everLong="+everLong);
// System.out.println("fileSize="+size+",dataLength="+dataLength);
}
long end = System.currentTimeMillis();
// System.out.println("fileSize="+size+",dataLength="+dataLength+",outFile1="+outFile1.length());
System.out.println("channel tranfer:" + (end - start));


long start1 = System.currentTimeMillis();
File outFile1 = new File("f:\\test\\array\\" + (new Random().nextInt()));
if (!outFile1.getParentFile().exists())
outFile1.getParentFile().mkdirs();
read(inFile, outFile1);
long end1 = System.currentTimeMillis();
System.out.println("array tranfer:" + (end1 - start1));


long start2 = System.currentTimeMillis();
File outFile2 = new File("f:\\test\\bytebuffer\\"
+ (new Random().nextInt()));
if (!outFile2.getParentFile().exists())
outFile2.getParentFile().mkdirs();
readByteBuffer(inFile, outFile2);
long end2 = System.currentTimeMillis();
System.out.println("readByteBuffer tranfer:" + (end2 - start2));


}


public static InputStream getBlockInputStream(File srcFile)
throws IOException {
RandomAccessFile blockInFile;
try {
blockInFile = new RandomAccessFile(srcFile, "r");
} catch (FileNotFoundException fnfe) {
throw new IOException("Expected block file at " + srcFile
+ " does not exist.");
}


return new FileInputStream(blockInFile.getFD());
}


public static OutputStream getOutStream(File targetFile) {
FileOutputStream blockOut = null;


try {
blockOut = new FileOutputStream(new RandomAccessFile(targetFile,
"rw").getFD());
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return blockOut;


}


public static void read(File srcFile, File desFile) throws IOException {
OutputStream out = getOutStream(desFile);
InputStream in = getBlockInputStream(srcFile);
byte buf[] = new byte[4 * 1024 * 1024];
int dataLen = 0;
int totalSize = 0;
dataLen = in.read(buf, 0, buf.length);
while (dataLen != -1) {
totalSize += dataLen;


out.write(buf, 0, dataLen);
dataLen = in.read(buf, 0, buf.length);
}
}


public static void readByteBuffer(File srcFile, File desFile)
throws IOException {
FileChannel out = ((FileOutputStream) getOutStream(desFile))
.getChannel();
FileChannel in = ((FileInputStream) getBlockInputStream(srcFile))
.getChannel();


ByteBuffer buf = ByteBuffer.allocate(4 * 1024 * 1024);
while (in.read(buf) != -1) {
buf.flip(); // 准备写
out.write(buf);
buf.clear(); // 准备读
}
}
}


读取4KB小文件:

channel transfer:1
array transfer:9
readByteBuffer transfer:8

读取206MB的大文件:

channel transfer:7118
array transfer:6298
readByteBuffer transfer:12148


<think>我们被要求提供一个使用多线程字节缓冲区在Java中实现文件复制的示例代码,涉及FileInputStream、FileOutputStream以及资源关闭的最佳实践。注意:多线程文件复制通常用于大文件,可以将文件分割成多个部分,每个线程复制一个部分。但这里需要注意,多线程复制文件可能并不会提高性能(因为磁盘I/O是顺序的,多线程可能反而降低性能),但在某些情况下(如目标磁盘支持并行写入)可能有用。不过,根据要求,我们将实现一个多线程复制文件的例子。然而,通常我们使用单线程复制文件时,使用一个缓冲区(字节数组)循环读取写入即可。多线程复制文件需要将文件分成多个块,每个线程负责读取一个块并写入到目标文件的相应位置。我们将使用RandomAccessFile来支持多线程同时写入文件的不同部分,或者使用FileChannel的transferTo方法(但transferTo通常用于单线程)。这里我们选择使用RandomAccessFile来实现多线程写入。步骤:1.将文件分成若干块(例如,根据线程数)。2.每个线程负责读取源文件的一个块(使用FileInputStream,并且每个线程使用自己的文件指针),并写入目标文件的对应位置(使用RandomAccessFile,定位到对应位置写入)。3.注意资源关闭:使用try-with-resources确保每个线程打开的资源都能关闭,或者在线程结束时关闭。但是,每个线程都打开同一个源文件的FileInputStream?这样可能会造成混乱。我们可以为每个线程打开一个FileInputStream,并跳过前面的部分。或者,我们可以使用一个FileInputStream,但多线程共享同一个输入流并同步读取?这样会降低性能。所以,每个线程打开自己的输入流,并使用skip跳过前面的部分。另一种方法是使用FileChannel,它支持从指定位置读取,我们可以让每个线程从不同的位置开始读取。但是FileChannel是线程安全的,所以我们可以多个线程共享同一个FileChannel,然后每个线程读取不同的位置。这样效率更高,因为打开多个输入流可能会降低性能。因此,我们使用FileChannel来实现多线程复制。每个线程将使用同一个FileChannel(线程安全)来读取不同的块,并使用FileChannel(或RandomAccessFile)来写入目标文件的不同位置。设计:1.使用FileInputStream获取FileChannel(源文件)。2.使用FileOutputStream获取FileChannel(目标文件)或者使用RandomAccessFile(目标文件)获取FileChannel,因为我们需要在文件的不同位置写入。3.将文件分成多个块(根据块大小或线程数)。4.创建多个线程,每个线程复制一个块。每个线程从源通道的指定位置读取数据,写入目标通道的指定位置。注意:FileChannel的读写是线程安全的,所以我们可以多个线程使用同一个通道。但是,目标通道的写入也需要保证在正确的偏移位置,我们可以使用FileChannel的position方法设置读取位置,然后使用transferTo(或transferFrom)方法,但是transferTo只能将数据从当前通道传输到另一个通道,而且是从当前通道的当前位置开始。所以我们可以使用transferTo,但需要为每个线程设置通道的位置?这需要同步,因为多个线程设置同一个通道的位置会冲突。所以,我们可以使用每个线程使用自己的ByteBuffer,并通过读取写入操作来指定位置。具体做法:每个线程使用以下方法:sourceChannel.read(ByteBuffer,position);//从源通道的指定位置读取到ByteBuffertargetChannel.write(ByteBuffer,position);//将ByteBuffer中的数据写入目标通道的指定位置这两个方法都是线程安全的(因为FileChannel是线程安全的),并且可以指定位置,这样就不需要改变通道的当前位置。因此,我们使用这种方式。资源关闭:使用try-with-resources关闭主通道,每个线程的ByteBuffer不需要关闭(因为ByteBuffer不是资源)。另外,我们也可以使用直接缓冲区(DirectByteBuffer)来提高性能,特别是大文件时。步骤:1.打开源文件的FileInputStreamFileChannel。2.打开目标文件的FileOutputStream(或RandomAccessFile)FileChannel。3.计算文件大小,然后根据线程数计算每个块的大小(最后一个块可能大小不同)。4.创建线程池,提交任务。5.每个任务:从源通道的指定位置读取一个块(大小为块大小,最后一个块为剩余大小)到ByteBuffer,然后将这个ByteBuffer写入目标通道的指定位置。6.等待所有任务完成,然后关闭通道。注意:由于我们使用同一个源通道目标通道,所以需要在所有任务完成后关闭通道。使用try-with-resources管理主资源,然后在线程池中执行任务,最后关闭线程池。代码结构:注意:由于我们打开两个通道(源目标),我们可以在try-with-resources中打开它们,这样确保即使任务中发生异常也能关闭通道。但是,任务是在线程池中执行的,而try-with-resources块会在主线程中关闭通道,所以必须确保所有任务都完成后再关闭通道。因此,我们可以在try块中提交任务,然后等待所有任务完成,最后try块结束时自动关闭通道。使用ExecutorService来管理线程。示例代码:注意:直接使用FileChannel的readwrite方法,并指定位置。我们将使用直接分配的ByteBuffer,以提高性能。但是,由于每个线程都需要一个ByteBuffer,我们可以为每个线程分配一个固定大小的ByteBuffer(比如8KB或1MB)。注意,如果块大小大于缓冲区大小,我们需要循环读取写入直到完成整个块的复制。因此,每个任务需要循环复制,直到分配给它的大小全部复制完成。具体任务逻辑:longblockSize=总大小/线程数;//最后一个线程处理剩余部分每个任务负责复制[start,end)区间的数据,start=线程id*blockSize,end=(线程id+1)*blockSize(最后一个线程为文件末尾)在任务中:longposition=start;while(position<end){intbytesToRead=(int)Math.min(bufferSize,end-position);//本次读取的字节数ByteBufferbuffer=ByteBuffer.allocateDirect(bytesToRead);intbytesRead=sourceChannel.read(buffer,position);//从源通道的position位置读取bytesToRead个字节if(bytesRead==-1){break;//文件结束}buffer.flip();//切换到读模式while(buffer.hasRemaining()){targetChannel.write(buffer,position);//写入到目标通道的相同位置(因为同一个文件块)}position+=bytesRead;}但是,这里我们使用了一个临时的直接缓冲区(每次循环都分配新的),这可能会影响性能。我们可以复用ByteBuffer,但是每个线程一个ByteBuffer,并且缓冲区大小固定(比如8KB),然后循环使用这个缓冲区直到复制完整个块。优化:每个任务只分配一个固定大小的ByteBuffer(比如8KB),然后循环读取写入,直到复制完整个块。修改任务:longposition=start;ByteBufferbuffer=ByteBuffer.allocateDirect(BUFFER_SIZE);while(position<end){intbytesToRead=(int)Math.min(BUFFER_SIZE,end-position);buffer.clear();//清空缓冲区,准备写入数据buffer.limit(bytesToRead);//设置本次读取的字节数intbytesRead=sourceChannel.read(buffer,position);//读取到缓冲区if(bytesRead==-1){break;}buffer.flip();//切换到读模式while(buffer.hasRemaining()){targetChannel.write(buffer,position);//注意:这里写入的位置也是position,因为文件块的位置是固定的}position+=bytesRead;}但是,注意:在写入的时候,我们指定了写入位置position,但是每次写入的字节数可能小于bytesToRead(因为write方法可能不会一次性写完),所以我们需要循环写入直到buffer中所有数据都写入。上面的内层循环已经做了。另外,我们也可以使用transferFrom或transferTo,但是它们不支持指定位置(需要改变通道的当前位置,而多个线程共享通道,改变当前位置需要同步,这样效率低)。所以使用上述方法。现在,我们编写代码:注意:我们使用try-with-resources来关闭源通道目标通道,以及文件输入输出流。同时,使用线程池,并在所有任务完成后关闭线程池。代码示例:</think>以下是使用多线程字节缓冲区实现Java文件复制的示例代码,重点包含字节数组缓冲区、多线程分块处理及资源关闭的最佳实践: ```java import java.io.*; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ConcurrentFileCopy { // 缓冲区大小 (1MB) private static final int BUFFER_SIZE = 1024 * 1024; // 线程数 (根据CPU核心数调整) private static final int THREAD_POOL_SIZE = Runtime.getRuntime().availableProcessors(); public static void main(String[] args) { String sourcePath = "source.dat"; String destPath = "target.dat"; try { long startTime = System.currentTimeMillis(); copyFile(sourcePath, destPath); long duration = System.currentTimeMillis() - startTime; System.out.println("文件复制完成! 耗时: " + duration + "ms"); } catch (IOException e) { e.printStackTrace(); } } public static void copyFile(String source, String dest) throws IOException { try (FileInputStream fis = new FileInputStream(source); FileOutputStream fos = new FileOutputStream(dest); FileChannel inChannel = fis.getChannel(); FileChannel outChannel = fos.getChannel()) { long fileSize = inChannel.size(); ExecutorService executor = Executors.newFixedThreadPool(THREAD_POOL_SIZE); // 计算每个线程处理的数据块大小 long chunkSize = (fileSize + THREAD_POOL_SIZE - 1) / THREAD_POOL_SIZE; for (long start = 0; start < fileSize; start += chunkSize) { long end = Math.min(start + chunkSize, fileSize); executor.submit(new CopyTask(inChannel, outChannel, start, end)); } executor.shutdown(); while (!executor.isTerminated()) { // 等待所有线程完成 } } // try-with-resources自动关闭所有资源 } static class CopyTask implements Runnable { private final FileChannel inChannel; private final FileChannel outChannel; private final long start; private final long end; public CopyTask(FileChannel inChannel, FileChannel outChannel, long start, long end) { this.inChannel = inChannel; this.outChannel = outChannel; this.start = start; this.end = end; } @Override public void run() { try { ByteBuffer buffer = ByteBuffer.allocateDirect(BUFFER_SIZE); long position = start; while (position < end) { buffer.clear(); // 限制本次读取的最大长度 int bytesToRead = (int) Math.min(BUFFER_SIZE, end - position); buffer.limit(bytesToRead); // 从指定位置读取数据 int bytesRead = inChannel.read(buffer, position); if (bytesRead == -1) break; // 翻转缓冲区准备写入 buffer.flip(); while (buffer.hasRemaining()) { outChannel.write(buffer, position); } position += bytesRead; } } catch (IOException e) { e.printStackTrace(); } } } } ``` ### 关键技术解析: 1. **多线程分块处理**: - 将大文件分割为多个数据块($chunkSize = \lceil \frac{fileSize}{threadCount} \rceil$) - 每个线程独立处理指定区间的文件块($[start, end)$) 2. **字节缓冲区优化**: - 使用直接缓冲区 `ByteBuffer.allocateDirect()` 减少JVM堆内存拷贝[^2] - 固定缓冲区大小(1MB)平衡内存开销与I/O效率 - 循环读写机制处理大于缓冲区大小的数据块 3. **资源关闭最佳实践**: - 使用 **try-with-resources** 自动关闭 `FileInputStream/FileOutputStream` 通道[^3] - 线程池 `ExecutorService` 显式关闭(`shutdown()` + 等待终止) - 避免在任务线程中单独关闭共享通道(由主资源块统一管理) 4. **通道定位写入**: - `FileChannel.write(buffer, position)` 确保多线程写入正确位置[^2] - 原子性写入操作避免数据错位 ### 性能优化点: 1. 直接缓冲区(DirectByteBuffer)减少内存复制开销 2. 线程池大小匹配CPU核心数($threadCount = cpuCores$) 3. 通道操作替代传统流操作提升吞吐量 4. 精确计算读写位置避免无效I/O > **注意**:多线程复制在SSD磁盘或RAID阵列上效果最佳,机械硬盘可能受限于磁头寻道时间。单线程复制小文件时,使用 `Files.copy()` 更简洁[^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值