强制停止在外部线程上运行的 Java Files.copy()
Posted
技术标签:
【中文标题】强制停止在外部线程上运行的 Java Files.copy()【英文标题】:Force stop Java Files.copy() running on external thread 【发布时间】:2017-06-22 09:57:16 【问题描述】:这里的答案似乎是 Java 8 之前的有效解决方案: How to cancel Files.copy() in Java?
但现在它不起作用,因为ExtendedCopyOption.INTERRUPTIBLE
是私有的。
基本上,我需要从给定的URL
下载一个文件,然后使用Files.copy()
将其保存到我的本地文件系统中。
目前,我正在使用 JavaFX 服务,因为我需要在 ProgressBar
中显示进度。
但是,如果操作时间过长,我不知道如何阻止运行Files.copy()
的线程。
至少不需要使用Thread.stop()
。即使Thread.interrupt()
也失败了。
如果互联网连接不可用,我还希望操作正常终止。
为了在没有可用互联网连接的情况下进行测试,我将拔下以太网电缆并在 3 秒后将其放回原处。
不幸的是,Files.copy()
仅在我放回以太网电缆时才返回,而我希望它立即失败。
正如我所见,Files.copy()
内部正在运行一个循环,这会阻止线程退出。
Tester
(正在下载OBS Studio exe):
/**
* @author GOXR3PLUS
*
*/
public class TestDownloader extends Application
/**
* @param args
*/
public static void main(String[] args)
launch(args);
@Override
public void start(Stage primaryStage) throws Exception
// Block From exiting
Platform.setImplicitExit(false);
// Try to download the File from URL
new DownloadService().startDownload(
"https://github.com/jp9000/obs-studio/releases/download/17.0.2/OBS-Studio-17.0.2-Small-Installer.exe",
System.getProperty("user.home") + File.separator + "Desktop" + File.separator + "OBS-Studio-17.0.2-Small-Installer.exe");
DownloadService
:
在FileChannel
中使用@sillyfly 评论并删除File.copy
似乎仅适用于调用Thread.interrupt()
,但当互联网不可用时它不会退出..
import java.io.File;
import java.net.URL;
import java.net.URLConnection;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.file.StandardOpenOption;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
/**
* JavaFX Service which is Capable of Downloading Files from the Internet to the
* LocalHost
*
* @author GOXR3PLUS
*
*/
public class DownloadService extends Service<Boolean>
// -----
private long totalBytes;
private boolean succeeded = false;
private volatile boolean stopThread;
// CopyThread
private Thread copyThread = null;
// ----
private String urlString;
private String destination;
/**
* The logger of the class
*/
private static final Logger LOGGER = Logger.getLogger(DownloadService.class.getName());
/**
* Constructor
*/
public DownloadService()
setOnFailed(f -> System.out.println("Failed with value: " + super.getValue()+" , Copy Thread is Alive? "+copyThread.isAlive()));
setOnSucceeded(s -> System.out.println("Succeeded with value: " + super.getValue()+" , Copy Thread is Alive? "+copyThread.isAlive()));
setOnCancelled(c -> System.out.println("Succeeded with value: " + super.getValue()+" , Copy Thread is Alive? "+copyThread.isAlive()));
/**
* Start the Download Service
*
* @param urlString
* The source File URL
* @param destination
* The destination File
*/
public void startDownload(String urlString, String destination)
if (!super.isRunning())
this.urlString = urlString;
this.destination = destination;
totalBytes = 0;
restart();
@Override
protected Task<Boolean> createTask()
return new Task<Boolean>()
@Override
protected Boolean call() throws Exception
// Succeeded boolean
succeeded = true;
// URL and LocalFile
URL urlFile = new URL(java.net.URLDecoder.decode(urlString, "UTF-8"));
File destinationFile = new File(destination);
try
// Open the connection and get totalBytes
URLConnection connection = urlFile.openConnection();
totalBytes = Long.parseLong(connection.getHeaderField("Content-Length"));
// --------------------- Copy the File to External Thread-----------
copyThread = new Thread(() ->
// Start File Copy
try (FileChannel zip = FileChannel.open(destinationFile.toPath(), StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE))
zip.transferFrom(Channels.newChannel(connection.getInputStream()), 0, Long.MAX_VALUE);
// Files.copy(dl.openStream(), fl.toPath(),StandardCopyOption.REPLACE_EXISTING)
catch (Exception ex)
stopThread = true;
LOGGER.log(Level.WARNING, "DownloadService failed", ex);
System.out.println("Copy Thread exited...");
);
// Set to Daemon
copyThread.setDaemon(true);
// Start the Thread
copyThread.start();
// -------------------- End of Copy the File to External Thread-------
// ---------------------------Check the %100 Progress--------------------
long outPutFileLength;
long previousLength = 0;
int failCounter = 0;
// While Loop
while ((outPutFileLength = destinationFile.length()) < totalBytes && !stopThread)
// Check the previous length
if (previousLength != outPutFileLength)
previousLength = outPutFileLength;
failCounter = 0;
else
++failCounter;
// 2 Seconds passed without response
if (failCounter == 40 || stopThread)
break;
// Update Progress
super.updateProgress((outPutFileLength * 100) / totalBytes, 100);
System.out.println("Current Bytes:" + outPutFileLength + " ,|, TotalBytes:" + totalBytes
+ " ,|, Current Progress: " + (outPutFileLength * 100) / totalBytes + " %");
// Sleep
try
Thread.sleep(50);
catch (InterruptedException ex)
LOGGER.log(Level.WARNING, "", ex);
// 2 Seconds passed without response
if (failCounter == 40)
succeeded = false;
// --------------------------End of Check the %100 Progress--------------------
catch (Exception ex)
succeeded = false;
// Stop the External Thread which is updating the %100
// progress
stopThread = true;
LOGGER.log(Level.WARNING, "DownloadService failed", ex);
//----------------------Finally------------------------------
System.out.println("Trying to interrupt[shoot with an assault rifle] the copy Thread");
// ---FORCE STOP COPY FILES
if (copyThread != null && copyThread.isAlive())
copyThread.interrupt();
System.out.println("Done an interrupt to the copy Thread");
// Run a Looping checking if the copyThread has stopped...
while (copyThread.isAlive())
System.out.println("Copy Thread is still Alive,refusing to die.");
Thread.sleep(50);
System.out.println("Download Service exited:[Value=" + succeeded + "] Copy Thread is Alive? "
+ (copyThread == null ? "" : copyThread.isAlive()));
//---------------------- End of Finally------------------------------
return succeeded;
;
有趣的问题:
1->What does java.lang.Thread.interrupt() do?
【问题讨论】:
如果线程没有进入循环,它将自动停止。现在,如果线程确实继续运行,则意味着它处于循环中,您似乎想使用Thread.interrupt()
来停止它。但这只有在线程中有任何中断时才有效,例如Thread.pause(x)
。否则 JVM 不知道何时中断线程,如果它在文件写入操作的中间中断,它就不健康了。
我不确定,但看起来您必须使用类似于FileChannel
的东西,类似于this question 中描述的内容。不过,这似乎有点矫枉过正,所以也许有更简单的方法。
@sillyfly 我认为您的选项会做得很好,对于连接丢失测试,可以使用超时,有点像this question。
我注意到您尚未接受任何答案,请考虑在某个时候这样做。
@GhostCat 我的朋友,没有答案是完全正确的。
【参考方案1】:
我强烈建议您使用FileChannel
。
它有transferFrom()
方法,当运行它的线程被中断时立即返回。
(这里的 Javadoc 说它应该引发 ClosedByInterruptException
,但它没有。)
try (FileChannel channel = FileChannel.open(Paths.get(...), StandardOpenOption.CREATE,
StandardOpenOption.WRITE))
channel.transferFrom(Channels.newChannel(new URL(...).openStream()), 0, Long.MAX_VALUE);
它也有可能比它的java.io
替代品表现得更好。
(然而,事实证明Files.copy()
的实现可能会选择委托给这个方法,而不是自己实际执行复制。)
这是一个可重用 JavaFX 服务的示例,可让您从 Internet 获取资源并将其保存到本地文件系统,如果操作时间过长,则会自动正常终止。
服务任务(由createTask()
生成)是文件通道 API 的用户。
单独的ScheduledExecutorService
用于处理时间限制。
始终坚持使用good practices 扩展Service
。
如果您选择使用这种高级方法,您将无法跟踪任务的进度。
如果连接不可用,transferFrom()
最终应该返回而不抛出异常。
启动服务(可以从任何线程完成):
DownloadService downloadService = new DownloadService();
downloadService.setRemoteResourceLocation(new URL("http://speedtest.ftp.otenet.gr/files/test1Gb.db"));
downloadService.setPathToLocalResource(Paths.get("C:", "test1Gb.db"));
downloadService.start();
然后再取消(否则时间到了会自动取消):
downloadService.cancel();
请注意,相同的服务可以重复使用,只需确保在重新启动之前将其重置:
downloadService.reset();
这是DownloadService
类:
public class DownloadService extends Service<Void>
private static final long TIME_BUDGET = 2; // In seconds
private final ScheduledExecutorService watchdogService =
Executors.newSingleThreadScheduledExecutor(new ThreadFactory()
private final ThreadFactory delegate = Executors.defaultThreadFactory();
@Override
public Thread newThread(Runnable r)
Thread thread = delegate.newThread(r);
thread.setDaemon(true);
return thread;
);
private Future<?> watchdogThread;
private final ObjectProperty<URL> remoteResourceLocation = new SimpleObjectProperty<>();
private final ObjectProperty<Path> pathToLocalResource = new SimpleObjectProperty<>();
public final URL getRemoteResourceLocation()
return remoteResourceLocation.get();
public final void setRemoteResourceLocation(URL remoteResourceLocation)
this.remoteResourceLocation.set(remoteResourceLocation);
public ObjectProperty<URL> remoteResourceLocationProperty()
return remoteResourceLocation;
public final Path getPathToLocalResource()
return pathToLocalResource.get();
public final void setPathToLocalResource(Path pathToLocalResource)
this.pathToLocalResource.set(pathToLocalResource);
public ObjectProperty<Path> pathToLocalResourceProperty()
return pathToLocalResource;
@Override
protected Task<Void> createTask()
final Path pathToLocalResource = getPathToLocalResource();
final URL remoteResourceLocation = getRemoteResourceLocation();
if (pathToLocalResource == null)
throw new IllegalStateException("pathToLocalResource property value is null");
if (remoteResourceLocation == null)
throw new IllegalStateException("remoteResourceLocation property value is null");
return new Task<Void>()
@Override
protected Void call() throws IOException
try (FileChannel channel = FileChannel.open(pathToLocalResource, StandardOpenOption.CREATE,
StandardOpenOption.WRITE))
channel.transferFrom(Channels.newChannel(remoteResourceLocation.openStream()), 0, Long.MAX_VALUE);
return null;
;
@Override
protected void running()
watchdogThread = watchdogService.schedule(() ->
Platform.runLater(() -> cancel());
, TIME_BUDGET, TimeUnit.SECONDS);
@Override
protected void succeeded()
watchdogThread.cancel(false);
@Override
protected void cancelled()
watchdogThread.cancel(false);
@Override
protected void failed()
watchdogThread.cancel(false);
【讨论】:
【参考方案2】:其他答案/cmets 没有涵盖一个重要方面;这是你的错误假设:
我想要的是在没有互联网连接时立即失败。
这并不容易。 TCP 堆栈/状态机实际上是一个相当复杂的东西。并且根据您的上下文(操作系统类型;TCP 堆栈实现,内核参数,...),可能会发生网络分区并且发送者在 15 分钟或更长时间内没有注意到的情况分钟 .收听here 了解更多详情。
换句话说:“只是拔掉插头”并不等于“立即断开”您现有的 TCP 连接。仅作记录:您无需手动插入电缆来模拟网络中断。在合理的测试设置中,像 iptables 又名防火墙之类的工具可以为您做到这一点。
【讨论】:
【参考方案3】:您似乎需要一个异步/可取消的 HTTP GET,这可能很难。
问题是,如果读取停止等待更多数据(电缆被拉出),它不会退出,直到套接字死亡或新数据进入。
您可以遵循一些路径,修改套接字工厂以设置良好的超时,使用带有超时的 http 客户端等。
我会看看Apache Http Components,它具有基于 java NIO 套接字的非阻塞 HTTP。
【讨论】:
以上是关于强制停止在外部线程上运行的 Java Files.copy()的主要内容,如果未能解决你的问题,请参考以下文章
JAVA-初步认识-第十四章-多线程-停止线程方式-定义标记