Java几种文件复制的测评

前言

Java复制文件的实现方式有很多种。
IO模式下:
	1.FileInputSTream+FileOutputStream
	2.BufferedReader+BufferedWriter 采用readLine方法
	3.BufferedReader+BufferedWriter 采用 read(char buffer[])方法
	4.InputStream+OutputStream
 NIO模式下:
	1.FileChannel+FileChannel 复制缓冲区
	2.FileChannel+FileChannel 管道直连
Java-Files工具类:
	1.Files.copy()

说明

此次测试使用了一个400M的文件测试,并未采用小文件或者超大文件测试。

代码

import lombok.Data;
import lombok.extern.slf4j.Slf4j;

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;


@Slf4j
@Data
public class FileCopyDemo {

    public static void fileInputStreamIoCopy(String source,String target) throws IOException {
        FileInputStream fis=null;
        FileOutputStream fos=null;
        try {
            fis = new FileInputStream(new File(source));
            fos = new FileOutputStream(new File(target));
            int ch;
            byte[] buffer = new byte[1024];
            while((ch=fis.read(buffer)) != -1){
                fos.write(buffer, 0, ch);
            }
            fos.flush();
        }finally {
            if(null != fos){
                fos.close();
            }
            if(null != fis){
                fis.close();
            }
        }
    }

    public static void bufferReaderLineIoCopy(String source,String target) throws IOException {
        BufferedReader br = null;
        BufferedWriter bw = null;
        try {
            br = new BufferedReader(new InputStreamReader(new FileInputStream(source)));
            bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(target)));
            String s;
            while((s=br.readLine())!=null){
                bw.write(s);
                    bw.newLine();
            }
            bw.flush();
        }finally {
            if(null != bw){
                bw.close();
            }
            if(null != br){
                br.close();
            }
        }
    }

    public static void bufferReaderByteIoCopy(String source,String target) throws IOException {
        BufferedReader br = null;
        BufferedWriter bw = null;
        try {
            br = new BufferedReader(new InputStreamReader(new FileInputStream(source)));
            bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(target)));
            char[] buffer = new char[1024];
            int bytesRead;
            while ((bytesRead = br.read(buffer)) != -1) {
                bw.write(buffer, 0, bytesRead);
            }
            bw.flush();
        }finally {
            if(null != bw){
                bw.close();
            }
            if(null != br){
                br.close();
            }
        }
    }

    public static void inputStreamByteIoCopy(String source,String target) throws IOException {
        InputStream br = null;
        OutputStream bw = null;
        try {
            br =new FileInputStream(source);
            bw =new FileOutputStream(target);
            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = br.read(buffer)) != -1) {
                bw.write(buffer, 0, bytesRead);
            }
            bw.flush();
        }finally {
            if(null != bw){
                bw.close();
            }
            if(null != br){
                br.close();
            }
        }
    }

    public static  void fileChannelBufferNioCopy(String source,String target) throws IOException {
        FileChannel inChannel = null;
        FileChannel outChannel = null;
        File file = new File(target);
        if(!file.exists()){
           Files.createFile(Paths.get(target));
        }
        try {
            RandomAccessFile inFile = new RandomAccessFile(source, "rw");
            inChannel = inFile.getChannel();
            RandomAccessFile outFile = new RandomAccessFile(target, "rw");
            outChannel = outFile.getChannel();
            ByteBuffer buffer=ByteBuffer.allocate(1024);
            while (inChannel.read(buffer)!=-1){
                buffer.flip();
                outChannel.write(buffer);
                buffer.clear();
            }
        }finally {
            if(null != inChannel){
                inChannel.close();
            }
            if(null != outChannel){
                outChannel.close();
            }
        }
    }

    public static  void fileChannelTransferNioCopy(String source,String target) throws IOException {
        FileChannel inChannel = null;
        FileChannel outChannel = null;
        File file = new File(target);
        if(!file.exists()){
            Files.createFile(Paths.get(target));
        }
        try {
            RandomAccessFile inFile = new RandomAccessFile(source, "rw");
            inChannel = inFile.getChannel();
            RandomAccessFile outFile = new RandomAccessFile(target, "rw");
            outChannel = outFile.getChannel();
            inChannel.transferTo(0,inChannel.size(),outChannel);
        }finally {
            if(null != inChannel){
                inChannel.close();
            }
            if(null != outChannel){
                outChannel.close();
            }
        }
    }

    public static void main(String[] args) throws IOException {
        String source = "D:/test/mail_foxymoon.sql";
        String target1 = "D:/test/mail_foxymoon-1.sql";
        String target2 = "D:/test/mail_foxymoon-2.sql";
        String target3 = "D:/test/mail_foxymoon-3.sql";
        String target4 = "D:/test/mail_foxymoon-4.sql";
        String target5 = "D:/test/mail_foxymoon-5.sql";
        String target6 = "D:/test/mail_foxymoon-6.sql";
        String target7 = "D:/test/mail_foxymoon-7.sql";
        String target8 = "D:/test/mail_foxymoon-7.sql";
        long l1 = System.currentTimeMillis();
        fileInputStreamIoCopy(source,target1);
        log.info("fileInputStreamIoCopy:总共耗时:{}",System.currentTimeMillis()-l1);

        long l2 = System.currentTimeMillis();
        bufferReaderLineIoCopy(source,target2);
        log.info("bufferReaderLineIoCopy:总共耗时:{}",System.currentTimeMillis()-l2);

        long l3 = System.currentTimeMillis();
        bufferReaderByteIoCopy(source,target3);
        log.info("bufferReaderByteIoCopy(1024):总共耗时:{}",System.currentTimeMillis()-l3);

        long l4 = System.currentTimeMillis();
        inputStreamByteIoCopy(source,target4);
        log.info("inputStreamByteIoCopy(1024):总共耗时:{}",System.currentTimeMillis()-l4);

        long l5 = System.currentTimeMillis();
        fileChannelBufferNioCopy(source,target5);
        log.info("fileChannelBufferNioCopy:总共耗时:{}",System.currentTimeMillis()-l5);

        long l6 = System.currentTimeMillis();
        fileChannelTransferNioCopy(source,target6);
        log.info("fileChannelTransferNioCopy :总共耗时:{}",System.currentTimeMillis()-l6);

        long l7 = System.currentTimeMillis();
        Files.copy(Paths.get(source),Paths.get(target7));
        log.info("Files.copy 底层是inputByteIoCopy(8096) 适用于确定目标文件不存在 否则报错:总共耗时:{}",System.currentTimeMillis()-l7);

        long l8 = System.currentTimeMillis();
        Files.copy(Paths.get(source),Paths.get(target8), StandardCopyOption.REPLACE_EXISTING);
        log.info("Files.copy 底层是inputByteIoCopy(8096) 存在则替换:总共耗时:{}",System.currentTimeMillis()-l8);
    }
}

测试结果

: fileInputStreamIoCopy:总共耗时:3428
: bufferReaderLineIoCopy:总共耗时:4618
: bufferReaderByteIoCopy(1024):总共耗时:4212
: inputStreamByteIoCopy(1024):总共耗时:2183
: fileChannelBufferNioCopy:总共耗时:2241
: fileChannelTransferNioCopy :总共耗时:816
: Files.copy 底层是inputByteIoCopy(8096) 适用于确定目标文件不存在 否则报错:总共耗时:734
: Files.copy 底层是inputByteIoCopy(8096) 存在则替换:总共耗时:1202
从结果上来看,NIO管道直连的方式最佳。也就是传说中的零拷贝技术。

分析

管道技术FileChannel和输入输出流,看起来都类似于一种通道,为什么性能差距未如此之大呢?
 这就不得不说说原理了。
 内存分为 内核态和用户态,内核态由操作内核管理,上下文为操作系统。用户态由应用程序管理,上下文为应用程序,例如JVM申请的内存。 		
 其中,数据从内核态→用户态,内核态→内核态都需要cpu进行调度。数据从磁盘到内核态是由DMA技术实现。
 那么传统输入输出流文件下载的服务器的过程是: 磁盘→(磁盘缓存区)内核态→(java变量)用户态→(网络缓存区)内核态,中间cpu参与了2次调度
 零拷贝技术的原理是:磁盘→(磁盘缓存区)内核态→(网络缓存区)内核态,cpu参与了1次调度
 所以:
 			 传统的Java输入输出流通道代表的是   磁盘--内核--用户态  
             管道技术FileChannel通道代表的是 磁盘--内核
 那么:
 			是不是用了管道FileChannel就代表用了NIO,零拷贝了呢?
 答案:
		   肯定不是,管道技术只是提供了NIO多路复用的可行性,且对比fileChannelBufferNioCopy和fileChannelTransferNioCopy 可知,在使用transferTo,transferFrom才是零拷贝技术,自己声明Buffer再拷贝,其实还是传统的输入输出流模式。