阿里十年高端架构师总结:最大化Java NIO和NIO.2的五种方法

ぃ、小莉子 提交于 2020-01-11 23:23:17

JavaNIO的目的是改善Java平台上I / O密集型杂项的编程。十年后,许多Java程序员仍然不知道如何充分利用NIO,甚至更少的人意识到Java SE 7引入了更多新的输入/输出API(NIO.2)。在本教程中,您将找到五个简单的示例,这些示例演示了NIO和NIO.2软件包在常见Java编程场景中的优势。阿里十年高端架构师总结:最大化Java NIO和NIO.2的五种方法

NIO和NIO.2对Java平台的主要贡献是提高Java应用程序开发的核心领域之一中的性能:输入/输出处理。两种软件包都不是特别容易使用的,每个Java I / O场景都不需要新的Input / Output API。但是,正确使用Java NIO和NIO.2可以节省一些常见I / O操作所需的时间。这就是NIO和NIO.2的超级能力。
本文介绍了五种相对简单的方法来利用它们:
变更通知者(因为每个人都需要听众)
选择器帮助多路复用
渠道-承诺与现实
内存映射-至关重要
字符编码和搜索
NIO环境
已有 10年历史的增强功能仍然是Java 的New Input / Output包吗?原因是对于许多工作的Java程序员而言,基本的Java I / O操作已经足够了。大多数Java开发人员不具备学习NIO为我们的日常工作。而且,NIO不仅仅是性能套件。相反,它是与Java I / O相关的功能的异构集合。NIO通过“更接近Java程序”来提高Java应用程序的性能,这意味着NIO和NIO.2 API公开了较低级别的系统操作系统(OS)入口点。NIO的权衡在于,它同时使我们能够更好地控制I / O,并要求我们比基本I / O编程更加谨慎。NIO的另一个方面是它对应用程序表现力的关注,我们将在随后的一些练习中使用它。
对于许多开发人员而言,在维护工作期间可能会首次遇到NIO:应用程序具有正确的功能,但响应速度很慢,因此有人建议使用NIO来加速它。NIO用于提高处理性能时会发光,但其结果将与基础平台紧密相关。(请注意,NIO依赖于平台。)如果您是首次使用NIO,它将需要您谨慎地进行测量。您可能会发现NIO加速应用程序性能的能力不仅取决于操作系统,还取决于特定的JVM,主机虚拟化上下文,大容量存储特性甚至数据。然而,将测量归纳起来可能很棘手。请牢记这一点,特别是如果您的目标是移动部署。
现在,不用多说,让我们探索NIO和NIO.5的五个重要功能。
1.更改通知者(因为每个人都需要听众)
Java应用程序性能是对NIO或NIO.2感兴趣的开发人员的共同吸引力。但是,以我的经验来看,NIO.2的文件更改通知程序是New Input / Output API中最引人注目的(如果不太流行)。
在以下情况下,许多企业级应用程序需要采取特定的措施:
文件上传到FTP文件夹
配置定义已更改
草稿文件已更新
发生另一个文件系统事件
这些都是变更通知或变更响应的示例。在Java的早期版本(和其他语言)中,轮询通常是检测变更事件的最佳方法。轮询是一种特殊的无限循环:检查文件系统或其他对象,将其与上一个已知状态进行比较,如果没有更改,请在短暂的间隔(例如一百毫秒或十秒)后再次检查。无限期继续循环。
NIO.2为我们提供了一种更好的表达变更检测的方法。清单1是一个简单的示例。
清单1. NIO.2中的更改通知
import java.nio.file.attribute.;
import java.io.
;
import java.util.*;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.List;

public class Watcher {
public static void main(String[] args) {
Path this_dir = Paths.get(".");
System.out.println("Now watching the current directory ...");

      try {
          WatchService watcher = this_dir.getFileSystem().newWatchService();
          this_dir.register(watcher, StandardWatchEventKinds.ENTRY_CREATE);

          WatchKey watckKey = watcher.take();

          List<WatchEvent< &64;>> events = watckKey.pollEvents();
          for (WatchEvent event : events) {
              System.out.println("Someone just created the file '" + event.context().toString() + "'.");

         }

     } catch (Exception e) {
         System.out.println("Error: " + e.toString());
     }
  }

}
编译此源,然后启动命令行可执行文件。在同一目录中,创建一个新文件;例如,您可能touch example1甚至copy Watcher.class example1。您应该看到以下更改通知消息:
Someone just create the file 'example1'.
这个简单的示例说明了如何开始使用Java访问NIO的语言设施。它还介绍了NIO.2的Watcher类,该类比基于轮询的传统I / O解决方案更直接,更易于使用以进行更改通知。
注意点
NIO的通知程序比以前的轮询循环使用起来容易得多,以至于它很容易忽略需求分析。但是,您应该在第一次使用侦听器时仔细考虑这些语义。例如,知道文件修改何时结束比知道何时开始修改更有用。这种分析需要格外小心,尤其是在FTP投递文件夹这样的常见情况下。NIO是一个功能强大的软件包,其中包含一些微妙的“陷阱”。它可以惩罚不速之客。
2.选择器和异步I / O:选择器帮助多路复用
NIO的新手有时会将其与“非阻塞输入/输出”相关联。NIO不仅仅是非阻塞I / O,但该错误是有道理的:Java中的基本I / O处于阻塞状态 -意味着它等待直到可以完成操作为止-而非阻塞或异步I / O就是最常用的NIO设施之一。
NIO的非阻塞I / O是基于事件的,如清单1中的文件系统侦听器所示。这意味着为I / O通道定义了选择器(或回调或侦听器),然后处理继续。当选择器上发生事件时(例如,当一行输入到达时),选择器“唤醒”并执行。所有这些都是在单个线程中实现的,这与典型的Java I / O形成了鲜明的对比。
清单2演示了在多端口网络echo-er中使用NIO选择器,该程序与Greg Travis在2003年创建的程序略有修改(请参阅参考资料)。Unix和类似Unix的操作系统长期以来一直具有选择器的有效实现,因此,这种网络程序对于Java编码的网络程序来说是一种性能良好的模型。
清单2. NIO选择器
import java.io.;
import java.net.
;
import java.nio.;
import java.nio.channels.
;
import java.util.*;

public class MultiPortEcho
{
private int ports[];
private ByteBuffer echoBuffer = ByteBuffer.allocate( 1024 );

public MultiPortEcho( int ports[] ) throws IOException {
  this.ports = ports;

  configure_selector();
}

private void configure_selector() throws IOException {
  // Create a new selector
  Selector selector = Selector.open();

  // Open a listener on each port, and register each one
  // with the selector
  for (int i=0; i<ports.length; ++i) {
    ServerSocketChannel ssc = ServerSocketChannel.open();
    ssc.configureBlocking(false);
    ServerSocket ss = ssc.socket();
    InetSocketAddress address = new InetSocketAddress(ports[i]);
    ss.bind(address);

    SelectionKey key = ssc.register(selector, SelectionKey.OP_ACCEPT);

    System.out.println("Going to listen on " + ports[i]);
  }

  while (true) {
    int num = selector.select();

    Set selectedKeys = selector.selectedKeys();
    Iterator it = selectedKeys.iterator();

    while (it.hasNext()) {
      SelectionKey key = (SelectionKey) it.next();

      if ((key.readyOps() & SelectionKey.OP_ACCEPT)
        == SelectionKey.OP_ACCEPT) {
        // Accept the new connection
        ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
        SocketChannel sc = ssc.accept();
        sc.configureBlocking(false);

        // Add the new connection to the selector
        SelectionKey newKey = sc.register(selector, SelectionKey.OP_READ);
        it.remove();

        System.out.println( "Got connection from "+sc );
      } else if ((key.readyOps() & SelectionKey.OP_READ)
        == SelectionKey.OP_READ) {
        // Read the data
        SocketChannel sc = (SocketChannel)key.channel();

        // Echo data
        int bytesEchoed = 0;
        while (true) {
          echoBuffer.clear();

          int number_of_bytes = sc.read(echoBuffer);

          if (number_of_bytes <= 0) {
            break;
          }

          echoBuffer.flip();

          sc.write(echoBuffer);
          bytesEchoed += number_of_bytes;
        }

        System.out.println("Echoed " + bytesEchoed + " from " + sc);

        it.remove();
      }

    }
  }
}

static public void main( String args[] ) throws Exception {
  if (args.length<=0) {
    System.err.println("Usage: java MultiPortEcho port [port port ...]");
    System.exit(1);
  }

  int ports[] = new int[args.length];

  for (int i=0; i<args.length; ++i) {
    ports[i] = Integer.parseInt(args[i]);
  }

  new MultiPortEcho(ports);
}

}
编译此源代码,然后使用诸如之类的调用从命令行启动它java MultiPortEcho 8005 8006。一旦MultiPortEchoer运行,就可以在端口8005和8006上运行一个简单的telnet或其他终端仿真器。您将看到该程序回显收到的字符-并在单个Java线程中完成!
3.渠道:承诺与现实
在NIO中,通道可以是任何可读写的对象。它的工作是抽象文件和套接字。NIO通道支持一致的方法集合,因此可以根据stdout实际使用的网络连接或其他通道来编程而无需特殊情况。通道共享Java基本I / O 流的这一特征。流提供阻塞的I / O;通道支持异步I / O。
尽管NIO因其性能优势而经常被提倡,但更准确地说是它具有高响应能力。在某些情况下,NIO实际上比基本的Java I / O 性能差。例如,对于小文件的简单顺序读写,简单的流实现可能比相应的面向事件的基于通道的编码快两倍或三倍。同样,非多路复用通道(即,单独线程中的通道)可能比在单个线程中注册其选择器的通道慢得多。
下次您需要根据与流或通道相关的尺寸来定义编程问题时,请尝试询问以下问题:
您必须读写多少个I / O对象?
不同的I / O对象之间是否存在自然顺序,或者它们都需要同时发生?
您的I / O对象是否仅持续很短的时间间隔,或者在整个过程的生命周期中可能会持续存在?
在单个线程或几个不同的线程中执行I / O是否更自然?
网络流量看起来可能与本地I / O相同,还是两者具有不同的模式?
这种分析是决定何时使用流或通道的良好实践。切记:NIO和NIO.2不能代替基本的I / O;他们只是补充它。
4.内存映射-重要之处
使用NIO时,最一致的显着性能改进涉及内存映射。内存映射是一项操作系统级的服务,它使文件的各个部分出于编程目的而出现,例如内存区域。
内存映射有许多后果和含义,比我在这里要介绍的要多。从较高的角度来看,它有助于使I / O以内存访问而不是文件访问的速度发生。前者通常比后者快两个数量级。清单3是NIO的内存映射功能的最小展示。
清单3. NIO中的内存映射
导入java 。io 。RandomAccessFile ; 导入java 。NIO 。MappedByteBuffer ; 导入java 。NIO 。渠道。FileChannel ;

公共类mem_map_example { 私有静态整数mem_map_size = 20 * 1024 * 1024 ; 私有静态字符串fn = “ example_memory_mapped_file.txt” ; 

  公共静态无效主体(字符串[] args )抛出异常{ RandomAccessFile memoryMappedFile = new RandomAccessFile (fn ,“ rw” );     

      //映射文件到存储器MappedByteBuffer 出= memoryMappedFile 。getChannel ()。映射(FileChannel 。MapMode 。READ_WRITE ,0 ,mem_map_size );

      //写入内存映射文件对(INT 我= 0 ; 我< mem_map_size ; 我++){ 出来。放((byte )'A' ); } 系统。出来。的println (“文件'” + FN + “'现在是” + 整数。的toString (mem_map_size )+ “字节满”。);

      //从内存映射文件读取。for (int i = 0 ; i < 30 ; i ++){ 系统。出来。打印((焦炭)了。让(我)); } 系统。出来。println (“ \ n从内存映射文件'” + fn + “' 读取完成。” ); } }

清单3中的小型模型快速创建了一个20兆字节的文件,example_memory_mapped_file.txt用字符A填充文件,然后读取文件的前30个字节。在实际情况下,内存映射不仅对于I / O的原始速度很有趣,而且还因为几个不同的读取器和写入器可以同时附加到同一文件映像而引起关注。该技术强大到足以危险,但如果使用得当,它可以实现极快的实现。华尔街交易业务众所周知地利用内存映射,以使竞争对手获得秒,甚至毫秒。
5.字符编码和搜索
我要在本文中介绍的NIO的最终功能是charset,用于在不同字符编码之间进行转换的软件包。甚至在NIO之前,Java都通过该getBytes方法内置了许多相同的功能。charset但是,我们欢迎使用它,因为它比较getBytes低的体系结构级别更灵活并且更易于实现,从而产生了卓越的性能。这对于必须对英语以外的其他语言的编码,排序规则和其他特征敏感的搜索特别有价值。
清单4显示了从Java的本机Unicode字符编码转换为Latin-1的示例。
清单4. NIO中的字符编码
String some_string = "This is a string that Java natively stores as Unicode.";
Charset latin1_charset = Charset.forName("ISO-8859-1");
CharsetEncode latin1_encoder = charset.newEncoder();
ByteBuffer latin1_bbuf = latin1_encoder.encode(CharBuffer.wrap(some_string));
请注意,Charsets和通道设计为可以很好地协同工作,以确保需要在内存映射,异步I / O和编码转换之间进行协作的程序能够充分执行。
最后,开发这么多年我也总结了一套学习Java的资料与面试题,如果你在技术上面想提升自己的话,可以关注我,私信发送领取资料或者在评论区留下自己的联系方式,有时间记得帮我点下转发让跟多的人看到哦。阿里十年高端架构师总结:最大化Java NIO和NIO.2的五种方法

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!