操作文件流时,异步流程删除文件成功问题

问题描述

业务中要求在下载文件时,不能删除文件。自测时发现下载文件时,即使把文件删除了,也能下载成功,下载的文件的md5值与删除前的md5值一样。

然而领导写的测试demo,发现在操作文件流时,删除文件并不能成功。

删除成功的测试demo

package com.test;

import cn.hutool.core.io.LineHandler;
import cn.hutool.core.lang.Filter;
import cn.hutool.core.lang.func.VoidFunc0;
import com.google.common.base.Predicate;
import lombok.SneakyThrows;
import org.apache.commons.io.IOUtils;

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class Test {
    static String fileName = "C:\\Users\\Administrator\\files\\1GB";
    static File file = new File(fileName);


    @SneakyThrows
    public static void main(String[] args) {
        Test test = new Test();

        CopyFile copyFile = new CopyFile();
        copyFile.start();

        new DeleteFileThread(file, copyFile).start();

        Thread.sleep(9000000);
    }

    static class CopyFile extends Thread{
        public CopyFile() {
            super();
        }

        @SneakyThrows
        @Override
        public void run() {
            String fileNameCopy = "C:\\Users\\Administrator\\files\\1GB_copy";
            System.out.println(file.toPath().getFileSystem());

            InputStream inputStream =  Files.newInputStream(file.toPath());
            OutputStream outputStream = Files.newOutputStream(new File(fileNameCopy).toPath());

            LockSupport.park();
            System.out.println("park :" + new Date());
            IOUtils.copyLarge(inputStream, outputStream);

            inputStream.close();
            outputStream.close();
        }
    }

    static class DeleteFileThread extends Thread{
        private File file;
        private CopyFile copyFile;
        public DeleteFileThread(File file, CopyFile copyFile) {
            this.file = file;
            this.copyFile = copyFile;
        }

        @Override
        public void run() {
            System.out.println(file.delete()); // 返回 true
            LockSupport.unpark(copyFile);
            System.out.println("unpark :" + new Date());
        }
    }
}

使用LockSupport的unpark与park方法来确保,复制文件的线程拿到文件流后,等待直至文件被删除。

删除失败demo

 InputStream inputStream =  new FileInputStream(file);

不同之处就在于获取的input流方法不一样。

排查

    public static InputStream newInputStream(Path path, OpenOption... options)
        throws IOException
    {
        return provider(path).newInputStream(path, options);
    }
    // 上述方法内部调用的newInputStream如下:
    public InputStream newInputStream(Path path, OpenOption... options)
        throws IOException
    {
        if (options.length > 0) {
            for (OpenOption opt: options) {
                // All OpenOption values except for APPEND and WRITE are allowed
                if (opt == StandardOpenOption.APPEND ||
                    opt == StandardOpenOption.WRITE)
                    throw new UnsupportedOperationException("'" + opt + "' not allowed");
            }
        }
        return Channels.newInputStream(Files.newByteChannel(path, options));
    }
// provider 方法:
    private static FileSystemProvider provider(Path path) {
        return path.getFileSystem().provider();
    }

与new FileInputStream()有两点不一样:

  • 使用了FileSystem
  • 实际上是通过channel来获取文件流的。

由于文件删除后,还能继续操作文件,应该是文件被缓存到某处了,而且由于测试时,堆空间大小设置的512MB,所以这个文件还不是缓存在堆内存中。

排查1:
查阅有关FileSystem的资料,想找到有关操作文件的描述信息是否有缓存,未找到。

排查2:
想通过控制变量发来确定是不是channel的使用会删除成功。
自己写的channel处理文件,依然是删除失败

FileInputStream stream = new FileInputStream(file);
 channel = stream.getChannel();

找到源码中获取channel的方法:
测试demo:

SeekableByteChannel channel = Files.newByteChannel(file.toPath());
...

byte[] buffer = new byte[8192];
long count;
int n;
for(count = 0L; -1 != (n = channel.read(ByteBuffer.wrap(buffer))); count += (long)n) {
    outputStream.write(buffer, 0, n);
}
channel.close();
outputStream.close();

最后删除成功,复制文件成功,所以大致确定是Files.newByteChannel(file.toPath());的原因了。
深入查看源码,直到发现了:
FileChannel var6 = WindowsChannelFactory.newFileChannel(var4.getPathForWin32Calls(), var4.getPathForPermissionCheck(), var2, var5.address());

继续查看newFileChannel内部:
FileDescriptor var6 = open(var0, var1, var5, var3);

继续查看open内部:
long var17 = WindowsNativeDispatcher.CreateFile(var0, var6, var7, var3, var9, var8);

static long CreateFile(String var0, int var1, int var2, long var3, int var5, int var6) throws WindowsException {
    NativeBuffer var7 = asNativeBuffer(var0);

    long var8;
    try {
        var8 = CreateFile0(var7.address(), var1, var2, var3, var5, var6);
    } finally {
        var7.release();
    }

    return var8;
}

通过NativeBuffer 这个类名大概能看出来,做了缓存。
删除后,还能下载成功的原因大概是因为此处做了缓存。
(备注:由于项目运行在linux系统上,本地调试是windows系统,所以相关实现类是windows的实现类。)

验证:
下载前,查看磁盘空间,下载时,执行删除操作后,再次查看磁盘空间,下载完成后,再次查看磁盘空间。df -h
发现下载过程中磁盘空间没有变化,下载完成后,/dev/sdb5 下的used空间确实变小了。

在删除后的下载过程中执行命令lsof | grep deleted 查看文件被删除了,但是还未释放空间的信息,也有对应的文件信息。下载完成后,再次查询就没有相关的文件信息了。

反思与总结

1、依然是未想到使用linux命令去查看,个人的排查到使用了缓存,仅根据NativeBuffer还不能确定是将缓存存在哪一块空间了;通过df -h可以得出在磁盘空间上;通过lsof | grep deleted 得出文件虽然被删除了,但其实是空间还未释放。

2、在linux设备上使用new FileInputStream(file);测试下载文件时,删除文件,结果删除成功了,但是本地windows上测试时,delete方法会返回false,删除失败,可能是系统不同导致的?

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值