问题描述
业务中要求在下载文件时,不能删除文件。自测时发现下载文件时,即使把文件删除了,也能下载成功,下载的文件的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,删除失败,可能是系统不同导致的?