由于这些文件通道是异步工作的,因此与常规I / O相比它们的性能很有意思。 第二部分处理诸如内存和CPU消耗之类的问题,并说明如何在高性能方案中安全地使用新的NIO.2通道。 您还需要了解如何在不丢失数据的情况下关闭异步通道,这是第三部分。 最后,在第四部分中,我们将研究并发性。
注意:我不会解释异步文件通道的完整API。 那里有足够的帖子在这方面做得很好。 我的帖子更深入地介绍了实用性和使用异步文件通道时可能遇到的问题。
好吧,足够模糊的谈话,让我们开始吧。 这是一个代码片段,它打开一个异步通道(第7行),将字节序列写入文件的开头(第9行),并等待结果返回(第10行)。 最后,在第14行中关闭通道。
public class CallGraph_Default_AsynchronousFileChannel {private static AsynchronousFileChannel fileChannel;public static void main(String[] args) throws InterruptedException, IOException, ExecutionException {try {fileChannel = AsynchronousFileChannel.open(Paths.get("E:/temp/afile.out"), StandardOpenOption.READ,StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.DELETE_ON_CLOSE);Future<Integer> future = fileChannel.write(ByteBuffer.wrap("Hello".getBytes()), 0L);future.get();} catch (Exception e) {e.printStackTrace();} finally {fileChannel.close();}}
}
异步文件通道调用的重要参与者
在继续研究代码之前,让我们快速介绍一下异步(文件)通道星系中涉及的概念。 图1中的调用图显示了对AsynchronousFileChannel类的open()方法的调用中的序列图。 FileSystemProvider封装所有操作系统详细信息。 为了逗大家,我在编写本文时正在使用Windows XP客户端。 因此,WindowsFileSystemProvider调用实际创建文件的WindowsChannelFactory并调用WindowsAsynchronousFileChannelImpl,后者返回其自身的实例。 最重要的概念是Iocp,即I / O完成端口。 它是用于执行多个同时异步输入/输出操作的API。 创建完成端口对象,并将其与许多文件句柄关联。 当在对象上请求I / O服务时,将通过排队到I / O完成端口的消息来指示完成。 不向其他请求I / O服务的进程通知I / O服务已完成,而是检查I / O完成端口的消息队列以确定其I / O请求的状态。 I / O完成端口管理多个线程及其并发。 从图中可以看出Iocp是AsynchronousChannelGroup的子类型。 因此,在JDK 7异步通道中,异步通道组被实现为I / O完成端口。 它拥有负责执行所请求的异步I / O操作的ThreadPool。 ThreadPool实际上封装了ThreadPoolExecutor,它执行Java 1.5以来的所有多线程异步任务执行管理。 对异步文件通道的写操作将导致对ThreadPoolExecutor.execute()方法的调用。
一些基准
查看性能总是很有趣。 异步非阻塞I / O必须快速,对吗? 为了找到该问题的答案,我进行了基准分析。 同样,我使用亨氏微小的基准框架来做到这一点。 我的机器是2.90 GHz的Intel Core i5-2310 CPU,具有四个内核(64位)。 在基准测试中,我需要一个基准。 我的基线是对普通文件的简单常规同步写入操作。 这是代码段:
public class Performance_Benchmark_ConventionalFileAccessExample_1 implements Runnable {private static FileOutputStream outputfile;private static byte[] content = "Hello".getBytes();public static void main(String[] args) throws InterruptedException, IOException {try {System.out.println("Test: " + Performance_Benchmark_ConventionalFileAccessExample_1.class.getSimpleName());outputfile = new FileOutputStream(new File("E:/temp/afile.out"), true);Average average = new PerformanceHarness().calculatePerf(new PerformanceChecker(1000, new Performance_Benchmark_ConventionalFileAccessExample_1()), 5);System.out.println("Mean: " + DecimalFormat.getInstance().format(average.mean()));System.out.println("Std. Deviation: " + DecimalFormat.getInstance().format(average.stddev()));} catch (Exception e) {e.printStackTrace();} finally {new SystemInformation().printThreadInfo(true);outputfile.close();new File("E:/temp/afile.out").delete();}}@Overridepublic void run() {try {outputfile.write(content); // append content} catch (IOException e) {e.printStackTrace();}}
}
正如您在第25行中看到的那样,基准测试将对普通文件执行一次写入操作。 这些是结果:
Test: Performance_Benchmark_ConventionalFileAccessExample_1
Warming up ...
EPSILON:20:TESTTIME:1000:ACTTIME:1014:LOOPS:365947
EPSILON:20:TESTTIME:1000:ACTTIME:1014:LOOPS:372298
Starting test intervall ...
EPSILON:20:TESTTIME:1000:ACTTIME:1000:LOOPS:364706
EPSILON:20:TESTTIME:1000:ACTTIME:1014:LOOPS:368309
EPSILON:20:TESTTIME:1000:ACTTIME:1014:LOOPS:370288
EPSILON:20:TESTTIME:1000:ACTTIME:1001:LOOPS:364908
EPSILON:20:TESTTIME:1000:ACTTIME:1014:LOOPS:370820
Mean: 367.806,2
Std. Deviation: 2.588,665
Total started thread count: 12
Peak thread count: 6
Deamon thread count: 4
Thread count: 5
以下代码段是另一个基准,该基准也向异步文件通道发出写操作(第25行):
public class Performance_Benchmark_AsynchronousFileChannel_1 implements Runnable {private static AsynchronousFileChannel outputfile;private static int fileindex = 0;public static void main(String[] args) throws InterruptedException, IOException {try {System.out.println("Test: " + Performance_Benchmark_AsynchronousFileChannel_1.class.getSimpleName());outputfile = AsynchronousFileChannel.open(Paths.get("E:/temp/afile.out"), StandardOpenOption.WRITE,StandardOpenOption.CREATE, StandardOpenOption.DELETE_ON_CLOSE);Average average = new PerformanceHarness().calculatePerf(new PerformanceChecker(1000,new Performance_Benchmark_AsynchronousFileChannel_1()), 5);System.out.println("Mean: " + DecimalFormat.getInstance().format(average.mean()));System.out.println("Std. Deviation: " + DecimalFormat.getInstance().format(average.stddev()));} catch (Exception e) {e.printStackTrace();} finally {new SystemInformation().printThreadInfo(true);outputfile.close();}}@Overridepublic void run() {outputfile.write(ByteBuffer.wrap("Hello".getBytes()), fileindex++ * 5);}
}
这是我的机器上上述基准测试的结果:
Test: Performance_Benchmark_AsynchronousFileChannel_1
Warming up ...
EPSILON:20:TESTTIME:1000:ACTTIME:1015:LOOPS:42667
EPSILON:20:TESTTIME:1000:ACTTIME:1015:LOOPS:193351
Starting test intervall ...
EPSILON:20:TESTTIME:1000:ACTTIME:1015:LOOPS:191268
EPSILON:20:TESTTIME:1000:ACTTIME:1015:LOOPS:186916
EPSILON:20:TESTTIME:1000:ACTTIME:1014:LOOPS:189842
EPSILON:20:TESTTIME:1000:ACTTIME:1014:LOOPS:191103
EPSILON:20:TESTTIME:1000:ACTTIME:1015:LOOPS:192005
Mean: 190.226,8
Std. Deviation: 1.795,733
Total started thread count: 17
Peak thread count: 11
Deamon thread count: 9
Thread count: 10
由于上面的代码片段执行相同的操作,因此可以肯定地说异步文件通道不一定比常规I / O更快。 我认为这是一个有趣的结果。 在单线程基准测试中很难将常规I / O和NIO.2相互比较。 引入NIO.2是为了在高度并发的场景中提供I / O技术。 因此,询问更快的速度(NIO或常规I / O)并不是一个正确的问题。 合适的问题是:什么是“更多并发”? 但是,就目前而言,以上结果表明:
当只有一个线程发出I / O操作时,请考虑使用常规I / O。
现在就足够了。 我已经解释了基本概念,还指出了常规I / O仍然存在。 在第二篇文章中,我将介绍使用默认异步文件通道时可能遇到的一些问题。 我还将展示如何通过应用一些更可行的设置来避免这些问题。
应用自定义线程池
异步文件处理并不是高性能的绿卡。 在上一篇文章中,我证明了常规I / O可以比异步通道更快。 应用NIO.2文件通道时,还需要了解一些其他重要事实。 默认情况下,在NIO.2文件通道中执行所有异步I / O任务的Iocp类由所谓的“缓存”线程池支持。 这是一个线程池,可以根据需要创建新线程,但是会在可用时重用以前构造的线程。 查看Iocp持有的ThreadPool类的代码。
public class ThreadPool {
...private static final ThreadFactory defaultThreadFactory = new ThreadFactory() {@Overridepublic Thread newThread(Runnable r) {Thread t = new Thread(r);t.setDaemon(true);return t;}};
...static ThreadPool createDefault() {...ExecutorService executor =new ThreadPoolExecutor(0, Integer.MAX_VALUE,Long.MAX_VALUE, TimeUnit.MILLISECONDS,new SynchronousQueue<Runnable>(),threadFactory);return new ThreadPool(executor, false, initialSize);}
...
}
默认通道组中的线程池被构造为ThreadPoolExecutor,最大线程数为Integer.MAX_VALUE,保持时间为Long.MAX_VALUE。 线程由线程工厂创建为守护程序线程。 如果所有线程都忙,则使用同步移交队列来触发线程创建。 此配置存在多个问题:
- 如果您在异步通道上突发执行写入操作,则将创建数千个工作线程,这可能会导致OutOfMemoryError:无法创建新的本机线程。
- 当JVM退出时,所有守护进程线程都将被放弃-最终不执行块,也不会取消堆栈。
在我的其他博客中,我解释了为什么无限制线程池会引起麻烦。 因此,如果您使用异步文件通道,则可以选择使用自定义线程池而不是默认线程池。 以下代码段显示了示例自定义设置。
ThreadPoolExecutor pool = new
ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(2500));
pool.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
AsynchronousFileChannel outputfile = AsynchronousFileChannel.open(Paths.get(FILE_NAME), new HashSet<Standardopenoption>
(Arrays.asList(StandardOpenOption.WRITE, StandardOpenOption.CREATE)), pool);
AsynchronousFileChannel的Javadoc指出,自定义执行程序应“至少[...]支持无限制的工作队列,并且不应在execute方法的调用者线程上运行任务”。 这是一个冒险的说法,只有在资源不成问题的情况下才是合理的,这种情况很少发生。 对于异步文件通道,请使用有限线程池。 您不会遇到线程太多的问题,也无法用工作队列任务来充斥您的堆。 在上面的示例中,您有五个线程执行异步I / O任务,并且工作队列可容纳2500个任务。 如果超过了容量限制,则拒绝执行处理程序将实现CallerRunsPolicy,在该处客户端必须同步执行写任务。 因为工作负载被“推回”到客户端并同步执行,所以这可能(极大地)降低系统性能。 但是,它也可以使您免受结果无法预测的更严重的问题的困扰。 最佳做法是使用有界线程池并保持线程池大小可配置,以便您可以在运行时进行调整。 同样,要了解有关可靠的线程池设置的更多信息,请参阅我的其他博客条目。
具有同步移交队列和未限制最大线程池大小的线程池可能会激进地创建新线程,因此,通过消耗(PC寄存器和Java堆栈)JVM的运行时内存,可能会严重损害系统稳定性。 异步任务的“时间越长”(经过的时间),您越有可能遇到此问题。
具有无限制工作队列和固定线程池大小的线程池可以激进地创建新的任务和对象,从而通过过多的垃圾回收活动消耗堆内存和CPU,从而严重损害系统稳定性。 异步任务越大(大小)越长(经过时间),您越有可能遇到此问题。
这就是将自定义线程池应用于异步文件通道的全部内容。 我在本系列的下一篇博客中将介绍如何安全地关闭异步通道而不丢失数据。
参考:测试平台上的Java 7#7:NIO.2文件通道–第1部分–简介,测试平台上的Java 7#8:NIO.2文件通道–第2部分–应用来自我们JCG合作伙伴 Niklas的自定义线程池。
翻译自: https://www.javacodegeeks.com/2012/04/java-7-8-nio2-file-channels-on-test.html