Java 7:在不丢失数据的情况下关闭NIO.2文件通道

关闭异步文件通道可能非常困难。 如果您将I / O任务提交到异步通道,则需要确保正确执行了任务。 实际上,出于多种原因,这对于异步通道可能是一个棘手的要求。 默认的通道组使用守护进程线程作为工作线程,这不是一个好选择,因为如果JVM退出,这些线程就会被放弃。 如果将自定义线程池执行程序与非守护线程一起使用,则需要自己管理线程池的生命周期。 如果不这样做,当主线程退出时,线程仅保持活动状态。 因此,JVM实际上根本不会退出,您可以执行的操作是杀死JVM。

关闭异步通道时,另一个问题在AsynchronousFileChannel的javadoc中提到:“在通道打开时关闭执行程序服务会导致未指定的行为。” 这是因为close()上的操作AsynchronousFileChannel问题的任务是模拟与挂起的I / O操作(在同一个线程池)的故障相关的执行服务AsynchronousCloseException 因此,如果您在先前关闭关联的执行程序服务时在异步文件通道实例上执行close() ,则会得到RejectedExecutionException

综上所述,安全配置文件通道并关闭该通道的建议方法如下:

public class SimpleChannelClose_AsynchronousCloseException {

  private static final String FILE_NAME = "E:/temp/afile.out";
  private static AsynchronousFileChannel outputfile;
  private static AtomicInteger fileindex = new AtomicInteger(0);
  private static ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue());

  public static void main(String[] args) throws InterruptedException, IOException, ExecutionException {
   outputfile = AsynchronousFileChannel.open(
   Paths.get(FILE_NAME),
   new HashSet(Arrays.asList(StandardOpenOption.WRITE, 
              StandardOpenOption.CREATE,StandardOpenOption.DELETE_ON_CLOSE)), pool);
   List> futures = new ArrayList<>();
   for (int i = 0; i < 10000; i++) {
    futures.add(outputfile.write(ByteBuffer.wrap("Hello".getBytes()), fileindex.getAndIncrement() * 5));
   }
   outputfile.close();
   pool.shutdown();
   pool.awaitTermination(60, TimeUnit.SECONDS);
   for (Future future : futures) {
    try {
     future.get();
    } catch (ExecutionException e) {
     System.out.println("Task wasn't executed!");
    }
   }
  }
}

在第6和7行中定义了定制线程池执行程序服务。在第10至13行中定义了文件通道。在第18至20行中,异步通道以有序方式关闭。 首先关闭通道本身,然后关闭执行程序服务,最后最重要的一点是线程等待线程池执行程序的终止。

尽管这是使用自定义执行程序服务关闭通道的安全方法,但还是引入了新问题。 客户端提交了异步写入任务(第16行),并且可能希望确保一旦成功提交了这些任务,这些任务肯定会被执行。 始终不等待Future.get()返回(第23行),因为在许多情况下,这将导致*异步*文件通道adurdum。 上面的代码段将返回很多“未执行任务!” 消息导致将写操作提交到通道后立即关闭通道(第18行)。 为了避免这种“数据丢失”,您可以实现自己的CompletionHandler并将其传递给请求的写操作。

public class SimpleChannelClose_CompletionHandler {
...
 public static void main(String[] args) throws InterruptedException, IOException, ExecutionException {
...
   outputfile.write(ByteBuffer.wrap("Hello".getBytes()), fileindex.getAndIncrement() * 5, "", defaultCompletionHandler);
...
 }

 private static CompletionHandler defaultCompletionHandler = new CompletionHandler() {
  @Override
  public void completed(Integer result, String attachment) {
   // NOP
  }

  @Override
  public void failed(Throwable exc, String attachment) {
  System.out.println("Do something to avoid data loss ...");
  }
 };
}

CompletionHandler.failed()方法(第16行)在任务处理期间捕获任何运行时异常。 您可以在此处实施任何补偿代码,以避免数据丢失。 处理关键任务数据时,最好使用CompletionHandler 但是*仍然*还有另一个问题。 客户端可以提交任务,但是他们不知道池是否将成功处理这些任务。 在这种情况下,成功表示已提交的字节实际上到达了目的地(硬盘上的文件)。 如果您想确保所有提交的任务在关闭前都已得到实际处理,则会有些棘手。 您需要一个“优美的”关闭机制,该机制要等到工作队列为空时才*实际上*先关闭通道和关联的执行程序服务(使用标准生命周期方法无法实现)。

引入GracefulAsynchronousChannel

我的最后一个片段介绍了GracefulAsynchronousFileChannel 您可以在我的Git存储库中获取完整的代码。 该通道的行为是这样的:保证处理所有成功提交的写操作,如果通道准备关闭,则抛出NonWritableChannelException 实现该行为需要两件事。 首先,您需要在ThreadPoolExecutor的扩展中实现afterExecute() ,该扩展在队列为空时发送信号。 这就是DefensiveThreadPoolExecutor所做的。

private class DefensiveThreadPoolExecutor extends ThreadPoolExecutor {

 public DefensiveThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
   LinkedBlockingQueue workQueue, ThreadFactory factory, RejectedExecutionHandler handler) {
  super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, factory, handler);
 }

 /**
  * "Last" task issues a signal that queue is empty after task processing was completed.
  */
 @Override
 protected void afterExecute(Runnable r, Throwable t) {
  if (state == PREPARE) {
   closeLock.lock(); // only one thread will pass when closer thread is awaiting signal
   try {
    if (getQueue().isEmpty() && state < SHUTDOWN) {
     System.out.println("Issueing signal that queue is empty ...");
     isEmpty.signal();
     state = SHUTDOWN; // -> no other thread can issue empty-signal
    }
   } finally {
    closeLock.unlock();
   }
  }
  super.afterExecute(r, t);
 }
}

afterExecute()方法(第12行)在每个处理的任务之后由处理给定任务的线程执行。 该实现在第18行中发送isEmpty信号。第二个需要您优雅地关闭一个通道的部分是AsynchronousFileChannelclose()方法的自定义实现。

/**
 * Method that closes this file channel gracefully without loosing any data.
 */
@Override
public void close() throws IOException {
 AsynchronousFileChannel writeableChannel = innerChannel;
 System.out.println("Starting graceful shutdown ...");
 closeLock.lock();
 try {
  state = PREPARE;
  innerChannel = AsynchronousFileChannel.open(Paths.get(uri),
    new HashSet(Arrays.asList(StandardOpenOption.READ)), pool);
  System.out.println("Channel blocked for write access ...");
  if (!pool.getQueue().isEmpty()) {
   System.out.println("Waiting for signal that queue is empty ...");
   isEmpty.await();
   System.out.println("Received signal that queue is empty ... closing");
  } else {
   System.out.println("Don't have to wait, queue is empty ...");
  }
 } catch (InterruptedException e) {
  Thread.interrupted();
  throw new RuntimeException("Interrupted on awaiting Empty-Signal!", e);
 } catch (Exception e) {
  throw new RuntimeException("Unexpected error" + e);
 } finally {
  closeLock.unlock();
  writeableChannel.force(false);
  writeableChannel.close(); // close the writable channel
  innerChannel.close(); // close the read-only channel
  System.out.println("File closed ...");
  pool.shutdown(); // allow clean up tasks from previous close() operation to finish safely
  try {
   pool.awaitTermination(1, TimeUnit.MINUTES);
  } catch (InterruptedException e) {
   Thread.interrupted();
   throw new RuntimeException("Could not terminate thread pool!", e);
  }
  System.out.println("Pool closed ...");
 }
}

研究该代码一段时间。 有趣的位在第11行中,其中的innerChannel被只读通道替换。 这将导致任何后续的异步写入请求均由于NonWritableChannelException而失败。 在第16行中, close()方法等待isEmpty信号发生。 在上一个写任务之后发送此信号时, close()方法将继续执行有序的关闭过程(第27页及其后的内容)。 基本上,代码在文件通道和关联的线程池之间添加了共享的生命周期状态。 这样,两个对象都可以在关闭过程中进行通信,并避免数据丢失。

这是使用GracefulAsynchronousFileChannel的日志记录客户端。

public class MyLoggingClient {
 private static AtomicInteger fileindex = new AtomicInteger(0);
 private static final String FILE_URI = "file:/E:/temp/afile.out";

 public static void main(String[] args) throws IOException {
  new Thread(new Runnable() { // arbitrary thread that writes stuff into an asynchronous I/O data sink

     @Override
     public void run() {
      try {
       for (;;) {
        GracefulAsynchronousFileChannel.get(FILE_URI).write(ByteBuffer.wrap("Hello".getBytes()),
          fileindex.getAndIncrement() * 5);
       }
      } catch (NonWritableChannelException e) {
       System.out.println("Deal with the fact that the channel was closed asynchronously ... "
         + e.toString());
      } catch (Exception e) {
       e.printStackTrace();
      }
     }
    }).start();

  Timer timer = new Timer(); // asynchronous channel closer
  timer.schedule(new TimerTask() {
   public void run() {
    try {
     GracefulAsynchronousFileChannel.get(FILE_URI).close();
     long size = Files.size(Paths.get("E:/temp/afile.out"));
     System.out.println("Expected file size (bytes): " + (fileindex.get() - 1) * 5);
     System.out.println("Actual file size (bytes): " + size);
     if (size == (fileindex.get() - 1) * 5)
      System.out.println("No write operation was lost!");
     Files.delete(Paths.get("E:/temp/afile.out"));
    } catch (IOException e) {
     e.printStackTrace();
    }
   }
  }, 1000);


 }
}

客户端启动两个线程,一个线程在无限循环中(第6行以下)发出写操作。 在处理一秒钟后,另一个线程异步关闭文件通道(第25 ff行)。 如果运行该客户端,那么将产生以下输出:

Starting graceful shutdown ...
Deal with the fact that the channel was closed asynchronously ... java.nio.channels.NonWritableChannelException
Channel blocked for write access ...
Waiting for signal that queue is empty ...
Issueing signal that queue is empty ...
Received signal that queue is empty ... closing
File closed ...
Pool closed ...
Expected file size (bytes): 400020
Actual file size (bytes): 400020
No write operation was lost!

输出显示参与线程的有序关闭过程。 日志记录线程需要处理通道异步关闭的事实。 处理排队的任务后,将关闭通道资源。 没有数据丢失,客户端发出的所有内容均已真正写入文件目标位置。 在这种正常的关闭过程中,没有AsynchronousClosedExceptionRejectedExecutionException

这就是安全关闭异步文件通道的全部方法。 完整的代码在我的Git存储库中 。 希望您喜欢它。 期待您的评论。

参考:来自我们JCG合作伙伴 Niklas的“ Java 7:关闭NIO.2文件通道而不会丢失数据”。

翻译自: https://www.javacodegeeks.com/2012/05/java-7-closing-nio2-file-channels.html

你可能感兴趣的:(Java 7:在不丢失数据的情况下关闭NIO.2文件通道)