NIO提升系统性能

  • 前言

  在软件系统中,I/O的速度要比内存的速度慢很多,因此I/O经常会称为系统的瓶颈。所有,提高I/O速度,对于提升系统的整体性能有很大的作用。

  在java标准的I/O中,是基于流的I/O的实现,即InputStream和OutPutStream,这种基于流的实现以字节为基本单元,很容易实现各种过滤器。

  NIO和new I/O的简称,在java1.4纳入JDK中,具有以下特征:

  1、为所有的原始类型提供(buffer)缓存支持;

  2、使用Charset作为字符集编码解码解决方案;

  3、增加了通道(Channel)对象,作为新的原始I/O抽象;

  4、支持锁和内存访问文件的文件访问接口;

  5、提供了基于Selector的异步网络I/O;

  NIO是基于块(Block)的,它以块为基本单位处理数据。在NIO中,最重要的两个组件是buffer缓冲和channel通道。缓冲是一块连续的内存区域,是NIO读写数据的中转站。通道表示缓冲数据的源头或目的地,它用于向缓冲读取或写入数据,是访问缓冲的接口。通道和缓冲的关系如图:

  

  • NIO中的Buffer类和Channel

  JDK为每一种java原生类型都提供了一种Buffer,除了ByteBuffer外,其他每一种Buffer都具有完全一样的操作,除了操作类型不一样以外。ByteBuffer可以用于绝大多数标准I/O操作的接口。

  在NIO中和Buffer配合使用的还有Channel。Channel是一个双向通道,既可以读也可以写。有点类似Stream,但是Stream是单向的。应用程序不能直接对Channel进行读写操作,而必须通过Buffer来进行。

  下面以一个文件复制为例,简单介绍NIO的Buffer和Channel的用法,代码如下:

 1 public class NioCopyFileTest {
 2     public static void main(String[] args) throws Exception {
 3         NioCopyFileTest.copy("test.txt", "test2.txt");
 4     }
 5
 6     public static void copy(String resource,String destination) throws Exception{
 7         FileInputStream fis = new FileInputStream(resource);
 8         FileOutputStream fos = new FileOutputStream(destination);
 9
10         FileChannel inputFileChannel = fis.getChannel();//读文件通道
11         FileChannel outputFileChannel = fos.getChannel();//写文件通道
12         ByteBuffer byteBuffer = ByteBuffer.allocate(1024);//读写数据缓冲
13         while(true){
14             byteBuffer.clear();
15             int length =  inputFileChannel.read(byteBuffer);//读取数据
16             if(length == -1){
17                 break;//读取完毕
18             }
19             byteBuffer.flip();
20             outputFileChannel.write(byteBuffer);//写入数据
21         }
22         inputFileChannel.close();
23         outputFileChannel.close();
24     }
25 }

  代码中注释写的很详细了,输入流和输出流都对应一个Channel通道,将数据通过读文件channel读取到缓冲中,然后再通过写文件channel写入到缓冲中。这样就完成了文件复制。注意:缓冲在文件传输中起到的作用十分大,可以缓解内存和硬盘之间的性能差异,提升系统性能。

  • Buffer的基本原理

  Buffer有三个重要的参数:位置(position)、容量(capactiy)和上限(limit)。这三个参数的含义如下图:

  下面例子很好的解释了Buffer的工作原理:

 1      ByteBuffer buffer = ByteBuffer.allocate(15);//设置缓冲区大小为15
 2         System.out.println("position:"+buffer.position()+"limit:"+buffer.limit()+"capacity"+buffer.capacity());
 3         for (int i = 0; i < 10; i++) {
 4             buffer.put((byte) i);
 5         }
 6         System.out.println("position:"+buffer.position()+"limit:"+buffer.limit()+"capacity"+buffer.capacity());
 7         buffer.flip();//重置position
 8         for (int i = 0; i < 5; i++) {
 9             System.out.println(buffer.get());
10         }
11         System.out.println("position:"+buffer.position()+"limit:"+buffer.limit()+"capacity"+buffer.capacity());
12         buffer.flip();
13         System.out.println("position:"+buffer.position()+"limit:"+buffer.limit()+"capacity"+buffer.capacity());

  以上代码,先分配了15个字节大小的缓冲区。在初始阶段,position为0,capacity为15,limit为15。注意,position是从0开始的,所以索引为15的位置实际上是不存在的。

  接着往缓冲区放入10个元素,position始终指向下一个即将放入的位置,所有position为10,capacity和limit依然为15。

  进行flip()操作,会重置position的位置,并且将limit设置到当前position的位置,这时Buffer从写模式进入读模式,这样就可以防止读操作读取到没有进行操作的位置。所有此时,position为0,limit为10,capacity为15。

  接着进行五次读操作,读操作会设置position的位置,所以,position为5,limit为10,capacity为15。

  在进行一次flip()操作,此时可想而知position为0,limit为5,capacity为15。

  • Buffer的相关操作

  Buffer是NIO中最核心的对象,它的一系列的操作和使用也需要重点掌握,这里简单概括一下,也可以参考相关API查看。

  1、Buffer的创建:

  buffer的常见有两种方式,使用静态方法allocate()从堆中分配缓冲区,或者从一个既有数组中创建缓冲区。

1 ByteBuffer buffer = ByteBuffer.allocate(1024);//从堆中分配
2 byte[] arrays = new byte[1024];//从既有数组中创建
3 ByteBuffer buffer2 = ByteBuffer.wrap(arrays);

  2、重置或清空缓冲区:

  buffer还提供了一些用于重置和清空缓冲区的方法:rewind(),clear(),flip()。它们的作用如下:

  3、读写缓冲区:

  对Buffer对象进行读写操作是Buffer最重要的操作,buffer提供了许多读写操作的缓冲区。具体参考API。

  4、标志缓冲区

  标志(mark)缓冲区是一个在数据处理时很有用的功能,它就像书签一样,可以在数据处理中随时记录当前位置,然后再任意时刻回到这个位置,从而简化或加快数据处理的流程。相关函数为:mark()和reset()。mark()用于记录当前位置,reset()用于恢复到mark标记的位置。

  代码如下:

 1 ByteBuffer buffer = ByteBuffer.allocate(15);//设置缓冲区大小为15
 2         for (int i = 0; i < 10; i++) {
 3             buffer.put((byte) i);
 4         }
 5         buffer.flip();//重置position
 6         for (int i = 0; i < buffer.limit(); i++) {
 7             System.out.print(buffer.get());
 8             if(i==4){
 9                 buffer.mark();
10                 System.out.print("mark at"+i);
11             }
12         }
13         System.out.println();
14         buffer.reset();
15         while(buffer.hasRemaining()){
16             System.out.print(buffer.get());
17 }

  输出结果:

1 01234mark at456789
2 56789

  5、复制缓冲区

  复制缓冲区是以原缓冲区为基础,生成一个完全一样的缓冲区。方法为:duplicate()。这个函数对于处理复杂的Buffer数据很有好处。因为新生成的缓冲区和元缓冲区共享相同的内存数据。并且,任意一方的改动都是互相可见的,但是两者又各自维护者自己的position、limit和capacity。这大大增加了程序的灵活性,为多方同时处理数据提供了可能。

  代码如下:

 1         ByteBuffer buffer = ByteBuffer.allocate(15);//设置缓冲区大小为15
 2         for (int i = 0; i < 10; i++) {
 3             buffer.put((byte) i);
 4         }
 5         ByteBuffer buffer2 = buffer.duplicate();//复制当前缓冲区
 6         System.out.println("after buffer duplicate");
 7         System.out.println(buffer);
 8         System.out.println(buffer2);
 9         buffer2.flip();
10         System.out.println("after buffer2 flip");
11         System.out.println(buffer);
12         System.out.println(buffer2);
13         buffer2.put((byte)100);
14         System.out.println("after buffer2 put");
15         System.out.println(buffer.get(0));
16         System.out.println(buffer2.get(0));             

  输出结果如下:

1 after buffer duplicate
2 java.nio.HeapByteBuffer[pos=10 lim=15 cap=15]
3 java.nio.HeapByteBuffer[pos=10 lim=15 cap=15]
4 after buffer2 flip
5 java.nio.HeapByteBuffer[pos=10 lim=15 cap=15]
6 java.nio.HeapByteBuffer[pos=0 lim=10 cap=15]
7 after buffer2 put
8 100
9 100

  6、缓冲区分片

  缓冲区分片使用slice()方法,它将现有的缓冲区创建新的子缓冲区,子缓冲区和父缓冲区共享数据,子缓冲区具有完整的缓冲区模型结构。当处理一个buffer的一个片段时,可以使用一个slice()方法取得一个子缓冲区,然后就像处理普通缓冲区一样处理这个子缓冲区,而无需考虑边界问题,这样有助于系统模块化。 

 1     ByteBuffer buffer = ByteBuffer.allocate(15);//设置缓冲区大小为15
 2         for (int i = 0; i < 10; i++) {
 3             buffer.put((byte) i);
 4         }
 5         buffer.position(2);
 6         buffer.limit(6);
 7         ByteBuffer subBuffer = buffer.slice();//复制缓冲区
 8         for (int i = 0; i < subBuffer.limit(); i++) {
 9             byte b = subBuffer.get(i);
10             b=(byte) (b*10);
11             subBuffer.put(i, b);
12         }
13         buffer.limit(buffer.capacity());
14         buffer.position(0);
15         for (int i = 0; i < buffer.limit(); i++) {
16             System.out.print(buffer.get(i)+" ");
17         }

  输出结果: 

1 0 1 20 30 40 50 6 7 8 9 0 0 0 0 0 

  7、只读缓冲区

  可以使用缓冲区对象的asReadOnlyBuffer()方法得到一个与当前缓冲区一致的,并且共享内存数据的只读缓冲区,只读缓冲区对于数据安全非常有用。使用只读缓冲区可以保证数据不被修改,同时,只读缓冲区和原始缓冲区是共享内存块的,因此,对于原始缓冲区的修改,只读缓冲区也是可见的。

  代码如下:

 1      ByteBuffer buffer = ByteBuffer.allocate(15);//设置缓冲区大小为15
 2         for (int i = 0; i < 10; i++) {
 3             buffer.put((byte) i);
 4         }
 5         ByteBuffer readBuffer = buffer.asReadOnlyBuffer();
 6         for (int i = 0; i < readBuffer.limit(); i++) {
 7             System.out.print(readBuffer.get(i)+" ");
 8         }
 9         System.out.println();
10         buffer.put(2, (byte)20);
11         for (int i = 0; i < readBuffer.limit(); i++) {
12             System.out.print(readBuffer.get(i)+" ");
13         }

  结果:

1 0 1 2 3 4 5 6 7 8 9 0 0 0 0 0
2 0 1 20 3 4 5 6 7 8 9 0 0 0 0 0 

  由此可见,只读缓冲区并不是原始缓冲区在某一时刻的快照,而是和原始缓冲区共享内存数据的。当修改只读缓冲区时,会报ReadOnlyBufferException异常。

  8、文件映射到内存:

  NIO提供了一种将文件映射到内存的方法进行I/O操作,它可以比常规的基于流的I/O快很多。这个操作主要是由FileChannel.map()方法实现的。

  使用文件映射的方式,将文本文件通过FileChannel映射到内存中。然后在内存中读取文件内容。还可以修改Buffer,将实际数据写到对应的硬盘中。

1      RandomAccessFile raf = new RandomAccessFile("D:\\test.txt", "rw");
2         FileChannel fc = raf.getChannel();
3         MappedByteBuffer mbf = fc.map(MapMode.READ_WRITE, 0, raf.length());//将文件映射到内存
4         while(mbf.hasRemaining()){
5             System.out.println(mbf.get());
6         }
7         mbf.put(0,(byte)98);//修改文件
8         raf.close();

  9、处理结构化数据

  NIO还提供了处理结构化数据的方法,称为散射和聚集。散射是将一组数据读入到一组buffer中,聚集是将数据写入到一组buffer中。聚集和散射的基本使用方法和对单个buffer操作的使用方法类似。这一组缓冲区类似于一个大的缓冲区。

  散射/聚集IO对处理结构化数据非常有用。例如,对于一个具有固定格式的文件的读写,在已知文件具体结构的情况下,可以构造若干个符合文件结构的buffer,使得各个buffer的大小恰好符合文件各段结构的大小。

  例如,将"姓名:张三,年龄:18",通过聚集写创建该文件,然后再通过散射都来解析。

 1 ByteBuffer nameBuffer = ByteBuffer.wrap("姓名:张三,".getBytes("utf-8"));
 2         ByteBuffer ageBuffer = ByteBuffer.wrap("年龄:18".getBytes("utf-8"));
 3         int nameLength = nameBuffer.limit();
 4         int ageLength = ageBuffer.limit();
 5         ByteBuffer[] bufs = new ByteBuffer[]{nameBuffer,ageBuffer};
 6         File file = new File("D:\\name.txt");
 7         if(!file.exists()){
 8             file.createNewFile();
 9         }
10         FileOutputStream fos = new FileOutputStream(file);
11         FileChannel channel = fos.getChannel();
12         channel.write(bufs);
13         channel.close();
14
15         ByteBuffer nameBuffer2 = ByteBuffer.allocate(nameLength);
16         ByteBuffer ageBuffer2 = ByteBuffer.allocate(ageLength);
17         ByteBuffer[] bufs2 = new ByteBuffer[]{nameBuffer2,ageBuffer2};
18         FileInputStream fis = new FileInputStream("D:\\name.txt");
19         FileChannel channel2 = fis.getChannel();
20         channel2.read(bufs2);
21         String name = new String(bufs2[0].array(),"utf-8");
22         String age = new String(bufs2[1].array(),"utf-8");
23
24         System.out.println(name+age);

  通过和通道的配合使用,可以简化Buffer对于结构化数据处理的难度。

  注意,ByteBuffer是将文件一次性读入内存再做处理,而Stream方式则是边读取文件边处理数据,这也是两者性能差异的主要原因。

  • 直接内存访问

  NIO的Buffer还提供了一个可以直接访问系统物理内存的类--DirectBuffer。普通的ByteBuffer依然在JVM堆上分配空间,其最大内存,受最大堆的限制。而DirecBuffer直接分配在物理内存中,并不占用堆空间。创建DirectBuffer的方法是:ByteBuffer.allocateDirect(capacity)。

  在对普通的ByteBuffer的访问,系统总会使用一个"内核缓冲区"进行间接操作。而ByteBuffer所处的位置,就相当于这个"内核缓冲区"。因此,DirecBuffer是一种更加接近底层的操作。

  DirectBuffer的访问速度远高于ByteBuffer,但是其创建和销毁所消耗的时间却远大于ByteBuffer。在需要频繁创建和销毁Buffer的场合,显然不适合DirectBuffer的使用,但是如果能将DirectBuffer进行复用,那么在读写频繁的场合下,它完全可以大幅度改善系统性能。

时间: 2024-12-05 03:02:25

NIO提升系统性能的相关文章

Java性能优化之使用NIO提升性能

在软件系统中,由于IO的速度要比内存慢,因此,I/O读写在很多场合都会成为系统的瓶颈.提升I/O速度,对提升系统整体性能有着很大的好处. 在Java的标准I/O中,提供了基于流的I/O实现,即InputStream和OutputStream.这种基于流的实现以字节为单位处理数据,并且非常容易建立各种过滤器. NIO是New I/O的简称,具有以下特性: 为所有的原始类型提供(Buffer)缓存支持: 使用 java.nio.charset.Charset 作为字符集编码解码解决方案: 增加通道(

架构师提升篇:分布式系统中,如何提升系统性能?

在分布式系统中,平衡业务计算的压力分布,减少网络上的数据流动,是一种提升性能的手段,请看下面的例子. 1)案例背景 某"机械设计研究所"历史上在管理模式上采用传统的层次化垂直结构.但是近年来,随着用户对产品更新换代的要求越来越快.质量要求越来越高,在竞争日益剧烈.外部压力日益增大的形势下,该所在管理模型上重新定位,打破长久以来形成的垂直结构,形成一种趋向于水平集成的业务模型,使企业能更专注于自己的业务特长,在产品研发时,能更好地利用国内更先进的技术力量,以实现合作方异地协同设计. 为此

03 | 系统设计目标(一):如何提升系统性能?

提到互联网系统设计,你可能听到最多的词儿就是“三高”,也就是“高并发”“高性能”“高可用”,它们是互联网系统架构设计永恒的主题. 原文地址:https://www.cnblogs.com/lakeslove/p/12287226.html

异步并发利器:实际项目中使用CompletionService提升系统性能的一次实践

场景 随着互联网应用的深入,很多传统行业也都需要接入到互联网.我们公司也是这样,保险核心需要和很多保险中介对接,比如阿里.京东等等.这些公司对于接口服务的性能有些比较高的要求,传统的核心无法满足要求,所以信息技术部领导高瞻远瞩,决定开发互联网接入服务,满足来自性能的需求. 概念 CompletionService将Executor和BlockingQueue的功能融合在一起,将Callable任务提交给CompletionService来执行,然后使用类似于队列操作的take和poll等方法来获

实际项目中使用CompletionService提升系统性能的一次实践

随着互联网应用的深入,很多传统行业也都需要接入到互联网.我们公司也是这样,保险核心需要和很多保险中介对接,比如阿里.京东等等.这些公司对于接口服务的性能有些比较高的要求,传统的核心无法满足要求,所以信息技术部领导高瞻远瞩,决定开发互联网接入服务,满足来自性能的需求. 概念 CompletionService将Executor和BlockingQueue的功能融合在一起,将Callable任务提交给CompletionService来执行,然后使用类似于队列操作的take和poll等方法来获得已完

基于Java NIO的Socket通信

Java NIO模式的Socket通信,是一种同步非阻塞IO设计模式,它为Reactor模式实现提供了基础. 下面看看,Java实现的一个服务端和客户端通信的例子. NIO模式的基本原理描述如下: 服务端打开一个通道(ServerSocketChannel),并向通道中注册一个选择器(Selector),这个选择器是与一些感兴趣的操作的标识(SelectionKey,即通过这个标识可以定位到具体的操作,从而进行响应的处理)相关联的,然后基于选择器(Selector)轮询通道(ServerSock

Atitit.提升软件Web应用程序 app性能的方法原理 h5 js java c# php python android .net

1. 提升单例有能力的1 2. 减少工作数量2 2.1. 减少距离2 2.2. 添加索引2 2.3. Dma api2 2.4. Cdn2 2.5. Cache2 2.6. Pool技术2 2.7. 减少HTTP请求数2 2.8.  ·更大的数据格式2 2.9. 循环展开2 2.10. 循环转置3 2.11. 提高Cache命中率3 2.12. 小组件替换大组件4 3. 并行处理4 3.1. 多线程4 3.2. 数据库分区4 4. 减少等候4 4.1.  2.流水线(Pipeline)4 4.2

5种调优Java NIO和NIO.2的方式

Java NIO(New Input/Output)——新的输入/输出API包——是2002年引入到J2SE 1.4里的.Java NIO的目标是提高Java平台上的I/O密集型任务的性能.过了十年,很多Java开发者还是不知道怎么充分利用NIO,更少的人知道在Java SE 7里引入了更新的输入/输出 API(NIO.2).这篇教程展示了5个在Java编程的一些常见场景里使用NIO和NIO.2包的简单示例. NIO和NIO.2对于Java平台最大的贡献是提高了Java应用开发中的一个核心组件的

监控系统性能利器--DT

一.背景 每次分析用户访谈的结果,用户对性能总是不满意.软件开发商不遗余力投入资源来提升系统性能,是乎没收到预期的效果.客户想了解整个系统用户的访问情况,而我们只能给出单个功能优化功能前后的效果,达不到用户期望. 二.提升用户性能体验面临的问题      1. 用户使用体验的问题:无法将用户的感知量化.可视化的呈现.缺乏用户行为的记录,用户投诉以后,很难还原问题的现场.      2. 运维人员面临的问题:不能确切知道整个链路出了问题,调整可能涉及到多个部门,现在的监控只是单点的监控,没有确保整