「进阶」Java NIO及三大组件

Java IO知识回顾

Java NIO简介

Java NIO(Java New IO)是1个全新的、 JDK 1.4后提供的 IO API。它提供了与标准IO不同的IO工作方式,可替代 标准Java IO 的IO API。

NIO的工作方式

  • Channels and Buffers(通道和缓冲区)标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
  • Asynchronous IO(异步IO)Java NIO可以让你异步的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。
  • Selectors(选择器)Java NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道

NIO与IO的区别

1、IO面向Stream(流),NIO面向Buffer(缓存)

  • 面向Stream:每次从流中读取一个或多个字节,直到读取所有字节,并没有缓存字节的地方。不能前后移动流中的数据(因为如果要前后移动从流中读取的数据,就需先将其缓存到一个缓存区中)。
  • 面向Buffer【更灵活】:数据读取到一个稍后处理的缓冲区,需要时即可在缓冲区中前后移动(注意:移动前首先需要检查是否该缓冲区中包含你需要处理的数据)。需要确保当更多数据读入缓冲区时,不会覆盖掉区中原有的尚未处理的数据。

2、IO流都是阻塞的,而NIO有非阻塞模式

(1)IO的流:当一个线程threadA使用IO调用read()/write()操作时,threadA被阻塞,直到一些数据被读取或写入完成,此过程中threadA不能做任何事。

(2)NIO的非阻塞模式

  • 非阻塞读:线程threadA从某channel发送请求读取数据时,threadA仅能得到目前可用的数据,若目前没有可用数据,那么threadA不会获取任何数据并可以先做别的事情,而不是保持阻塞,直到有可用数据在这个通道出现。
  • 非阻塞写:一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。

线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出channels。

3、NIO独有选择器(Selector)

NIO核心组件

Java NIO的核心组件包括:

  • 通道(Channel)
  • 缓冲区(Buffer)
  • 选择器(Selectors)

普通的IO是面向流(Stream Oriented),而NIO则是面向缓冲区(Buffer Oriented)。IO流是单向的,直接面向字节流,通过InputStream、OutputStream来完成数据的输入输出。而NIO是双向的,通过建立通道(Channel),然后将数据装在缓冲区(Buffer)在通道上进行传输。针对不同类型的数据有不同的Buffer,根据数据类型不同(boolean 除外),提供了相应类型的缓冲区: ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、 FloatBuffer、DoubleBuffer。

Buffer的数据存取

1、创建Buffer实例

ByteBuffer buf = ByteBuffer.allocate(1024); //1024为capacity,通过allocate()方法可以获取一个缓冲区

2、Buffer类属性解析

Buffer类中有三个属性必须理解:capacity(容量)、limit(访问范围)、position(位置,表示缓冲区中正在操作数据的位置)。通过get(),put()方法进行数据的存取。通过flip()方法切换成读模式,clear()方法清空缓冲区。但是缓冲区中的数据依然存在,只是处于"被遗忘"状态,rewind()可重复读。

3、Buffer的分类

Buffer分为直接缓冲区和非直接缓冲区。非直接缓冲区 通过allocate()方法分配缓冲区,将缓冲区建立在JVM的内存中。直接缓冲区 通过allocateDirect()方法,将缓冲区建立在物理内存中。这样做可以提高IO效率,节省了copy的过程,直接缓冲区是物理内存映射文件,但是写入过程不受控制,读过程受GC影响!

通道的原理与获取

1、通道的原理

传统的javaIO是通过DMA的方式存取,这种方式需要CPU的权限。而通道(Channel)自带处理器,不需要去访问CPU,所以在进行大量IO时效率更高一些。通道用于源节点与目标节点的连接。在 Java NIO 中负责缓冲区中数据的传输,Channel 本身不存储数据,因此需要配合缓冲区进行传输。

2、通道的获取

通道的主要实现类 java.nio.channels.Channel 接口:FileChannel、SocketChannel、ServerSocketChannel、DatagramChannel

获取通道的方法:

  • 各IO有自己的获取方法
  • jdk1.7的NIO2,针对各个通道提供了静态方法open();
  • jdk1.7的NIO2的Files工具类的newByteChannel();

通道的数据传输

分散(Scatter)与聚集(Gather)

  • 分散读取(Scattering Reads),将Channel中读取的数据分散到Buffer
  • 聚集写入(Gathering Writes),将多个Buffer中的数据聚集到Channel中
RandomAccessFile raf1 = new RandomAccessFile("1.txt", "rw");       
//1. 获取通道
FileChannel channel = raf1.getChannel();     
//2. 分配指定大小的缓冲区
ByteBuffer buf1 = ByteBuffer.allocate(250);
ByteBuffer buf2 = ByteBuffer.allocate(500);        
//3. 分散读取
ByteBuffer[] bufs = {buf1, buf2};
channel1.read(bufs);      
for (ByteBuffer byteBuffer : bufs) {
    byteBuffer.flip();
}
System.out.println(new String(bufs[0].array(), 0, bufs[0].limit()));
System.out.println("-----------------");
System.out.println(new String(bufs[1].array(), 0, bufs[1].limit()));       
//4. 聚集写入
RandomAccessFile raf2 = new RandomAccessFile("2.txt", "rw");
FileChannel channel2 = raf2.getChannel();   
channel2.write(bufs);

NIO示例

示例1:基于通道 & 缓冲数据

// 1. 获取数据源 和 目标传输地的输入输出流(此处以数据源 = 文件为例)
FileInputStream fin = new FileInputStream(infile);
FileOutputStream fout = new FileOutputStream(outfile);

// 2. 获取数据源的输入输出通道
FileChannel fcin = fin.getChannel();
FileChannel fcout = fout.getChannel();

// 3. 创建 缓冲区 对象:Buffer(共有2种方法)
 // 方法1:使用allocate()静态方法
 ByteBuffer buff = ByteBuffer.allocate(256);
 // 上述方法创建1个容量为256字节的ByteBuffer
 // 注:若发现创建的缓冲区容量太小,则重新创建一个大小合适的缓冲区

// 方法2:通过包装一个已有的数组来创建
 // 注:通过包装的方法创建的缓冲区保留了被包装数组内保存的数据
 ByteBuffer buff = ByteBuffer.wrap(byteArray);

 // 额外:若需将1个字符串存入ByteBuffer,则如下
 String sendString="你好,服务器. ";
 ByteBuffer sendBuff = ByteBuffer.wrap(sendString.getBytes("UTF-16"));

// 4. 从通道读取数据 & 写入到缓冲区
// 注:若 以读取到该通道数据的末尾,则返回-1
fcin.read(buff);

// 5. 传出数据准备:将缓存区的写模式 转换->> 读模式
buff.flip();

// 6. 从 Buffer 中读取数据 & 传出数据到通道
fcout.write(buff);

// 7. 重置缓冲区
// 目的:重用现在的缓冲区,即 不必为了每次读写都创建新的缓冲区,在再次读取之前要重置缓冲区
// 注:不会改变缓冲区的数据,只是重置缓冲区的主要索引值
buff.clear();

示例2:基于选择器(Selecter)

// 1. 创建Selector对象   
Selector sel = Selector.open();

// 2. 向Selector对象绑定通道   
 // a. 创建可选择通道,并配置为非阻塞模式   
 ServerSocketChannel server = ServerSocketChannel.open();   
 server.configureBlocking(false);   
 
 // b. 绑定通道到指定端口   
 ServerSocket socket = server.socket();   
 InetSocketAddress address = new InetSocketAddress(port);   
 socket.bind(address);   
 
 // c. 向Selector中注册感兴趣的事件   
 server.register(sel, SelectionKey.OP_ACCEPT);    
 return sel;

// 3. 处理事件
try {    
    while(true) { 
        // 该调用会阻塞,直到至少有一个事件就绪、准备发生 
        selector.select(); 
        // 一旦上述方法返回,线程就可以处理这些事件
        Set<SelectionKey> keys = selector.selectedKeys(); 
        Iterator<SelectionKey> iter = keys.iterator(); 
        while (iter.hasNext()) { 
            SelectionKey key = (SelectionKey) iter.next(); 
            iter.remove(); 
            process(key); 
        }    
    }    
} catch (IOException e) {    
    e.printStackTrace();   
}

示例3:实现文件复制

实现方式:通道FileChannel、 缓冲区ByteBuffer

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class Test {

    public static void main(String[] args) throws IOException {
        // 设置输入源 & 输出地 = 文件
        String infile = "C:\\copy.sql";
        String outfile = "C:\\copy.txt";

        // 1. 获取数据源 和 目标传输地的输入输出流(此处以数据源 = 文件为例)
        FileInputStream fin = new FileInputStream(infile);
        FileOutputStream fout = new FileOutputStream(outfile);

        // 2. 获取数据源的输入输出通道
        FileChannel fcin = fin.getChannel();
        FileChannel fcout = fout.getChannel();

        // 3. 创建缓冲区对象
        ByteBuffer buff = ByteBuffer.allocate(1024);
        
        while (true) {

            // 4. 从通道读取数据 & 写入到缓冲区
            // 注:若 以读取到该通道数据的末尾,则返回-1  
            int r = fcin.read(buff);
            if (r == -1) {
                break;
            }
            // 5. 传出数据准备:调用flip()方法  
            buff.flip();
            
            // 6. 从 Buffer 中读取数据 & 传出数据到通道
            fcout.write(buff);
            
            // 7. 重置缓冲区
            buff.clear();
            
          }
        }

}

NIO常用API

核心接口与类关系图解

首先,图中有颜色背景的,是NIO中最重要的几个概念:Selector选择器、Channel通道、Buffer缓冲区、Pipe管道,它们几个的良好分工合作,才有了NIO。

这四者的关系:

  1. Channel就是管理文件/数据传输出入口的地方【水龙头 / 百姓家】。文件数据/客户端数据/服务端数据【水】 在需要开始传递时【水要从水管出来】,就先通过Channel【水龙头打开】;
  2. Buffer用于暂存从Channel传来的数据【水厂】,从而提供外部选择和调整数据集合的能力【管理桶装水】。当数据量大于Buffer的最大装载量时,Buffer中原有的数据将被覆盖。Channel的实现类通过read(Buffer)等方法将数据保存到Buffer中,又通过write(Buffer)将Buffer中存储的数据写入到通道中【桶装水最终会按需派送到各个百姓家中】。
  3. Selector 用于控制和管理一或多个Channel的数据流动【桶装水管理搬运工,负责自己管理的若干水龙头,哪里有山泉水要出了,就关好别的水龙头,拿水桶去装水并打上备注即立刻回去管理水龙头,待会桶装水会被同事送到水厂】。
  4. Pipe 用于在两个线程之间传输数据。其内部依赖着两个Channel对象,分别用于数据的收和发。
Channel的相关实现类:FileChannel、SocketChannel与ServerSocketChannel、DatagramChannel,分别对应:“文件操作通道”、“TCP通信操作通道”、“UDP通信操作通道”。这几个实现类中,除了FileChannel不能进入非阻塞状态,其他实现类都可以进入非阻塞状态。

所谓 阻塞状态(BIO)和非阻塞状态(NBIO),前面已解释。

图中的“分散”和“聚集”,分别指:

  1. 分散(scatter):【读】从channel中读取数据到buffer时,一个channel可以将数据缓存到多个buffer中。(read(Buffer[]);:开始存储数据到第一个buffer中,当一个buffer被写满后,channel紧接着向下一个buffer中写(按照缓冲区数组的排列顺序))
  2. 聚合(gather):【写】将buffer中的数据写入到channel中时,可以连续将多个buffer中的数据依次写入。

Buffer的相关实现类:

  1. 各种基本数据类型的Buffer,如:ByteBuffer、IntBuffer、ShortBuffer、LongBuffer、DoubleBuffer等,对应初始化时设定的capacity(缓冲区大小)即:最多同时缓存XX个byte、int、short、long、double数据。
  2. MappedByteBuffer:表示内存映射文件
Channel的原理:打开文件并构建符合NIO读写规则的通信桥梁口,对于网络TCP和UDP连接则是构建一个连接到特定IP特定端口的桥梁,并准备数据发送与数据接收。Channel与Stream(流)不同的是,Stream是单向传递数据的,而Channel是可读取并可写入的,具有双向性,并且更容易配合缓冲区来灵活获取数据。

Buffer的原理:Buffer 实际上是指向一个占N个单位的内存空间的对象,它本身就代表了一块内存区域。Buffer有两个模式:读模式和写模式(初始模式),通过flip()方法可以切换状态。而Buffer内部有三个成员属性用于共同维护这块内存区域,它们分别是:capacity【buffer总大小】、position【写模式:下一个可插入数据的位置,初始为0,最大是cap-1;读模式:下一个可读取数据的位置,初始为0,最大cap-1】、limit【读模式:最多可写入limit=cap个数据;写模式:可读取第limit=position(之前写入)的所有数据】。

NIO常用API清单

Channel通用方法

  • int read(Buffer):将数据从channel读取到buffer中【读channel,写buffer】
  • int read(Buffer[]):将数据从channel读取到buffer数组中
  • int write(Buffer):将数据从buffer写入到channel中【读buffer,写channel】
  • int write(Buffer[]):将数据从buffer数组写入到channel中

Buffer子类(以ByteBuffer为例)通用方法

各个Buffer子类都提供了静态方法allocate()进行创建Buffer实例,获取一个缓冲区。

//1024为capacity,通过allocate()方法可以获取一个缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
  • byte[] get():读取buffer中的所有数据
  • void put(byte[]):数据写入buffer【功能和从channel中读取数据到buffer中一样】
  • void filp():切换模式(写模式->读模式)
  • void rewind():重读buffer中的数据(position重置为0)
  • void clear():清空。重置所有指针,不删除数据!!(position=0,limit=capacity,重新供写入)
  • void compact():半清空,保留仍未读取的数据。(position=最后一个未读单元之后的位置,limit=cap,重新供写入)
  • mark():标记时刻A的当前pos【与reset()一起用】
  • reset():回到时刻A时标记的pos位置。
  • close():关闭并释放channel对象。

FileChannel

获取FileChannel对象:

RandomAccessFile accessFile = new RandomAccessFile(fileName, "rw");
FileChannel fileChannel = accessFile.getChannel();

SocketChannel

创建SocketChannel对象:

SocketChannel sc  = SocketChannel.open();

设置非阻塞IO状态:

sc.configureBlocking(false);

非阻塞状态下,成功连接前,干别的事:

sc.configureBlocking(false);
///...............
while(!sc.finishConnect()){   ////do other sth.... }

保证非阻塞IO状态下,read()过程不会read 空数据:

sc.configureBlocking(false);
///...............
while((int len = sc.read(buf))==0){   ////do other sth.... }

保证非阻塞IO状态下,write()过程不会write空数据:

sc.configureBlocking(false);
///...............
while((int len = sc.write(buf))==0){   ////do other sth.... }

ServerSocketChannel、DatagramChannel、Selector和Pipe的写法与方法说明,由于篇幅原因,可以参考下文的示例。

文件IO FileChannel

读文件:

public static byte[] readBytes(String fileName) {
  try {
	  ///获取对应文件的FileChannel对象
	  RandomAccessFile accessFile = new RandomAccessFile(fileName, "rw");
	  FileChannel fileChannel = accessFile.getChannel();
	  /// 创建一个缓冲区(大小为48byte)
	  ByteBuffer byteBuffer = ByteBuffer.allocate(48);
	  StringBuilder builder = new StringBuilder();

	  int bytesRead = fileChannel.read(byteBuffer);
	  while (bytesRead != -1) {
		  System.out.println("Read " + bytesRead);
		  ///翻转buffer
		  byteBuffer.flip();
		  ///每次读取完之后,输出缓存中的内容
		  while (byteBuffer.hasRemaining()) {
			  System.out.println((char) byteBuffer.get());
			  builder.append((char) byteBuffer.get());
		  }
		  ///然后清空缓存区
		  byteBuffer.clear();
		  ///重新再读数据到缓存区中
		  bytesRead = fileChannel.read(byteBuffer);
	  }

	  accessFile.close();
	  return builder.toString().getBytes();
  } catch (IOException e) {
	  e.printStackTrace();
	  return null;
  }
}

写文件:

public static void writeBytes(String fileName, byte[] data) {
  try {
      RandomAccessFile accessFile = new RandomAccessFile(fileName, "rw");
      FileChannel channel = accessFile.getChannel();
      ByteBuffer buffer = ByteBuffer.allocate(48);
      buffer.put(data);
      channel.write(buffer);
  } catch (FileNotFoundException e) {
      e.printStackTrace();
  } catch (IOException e) {
      e.printStackTrace();
  }
}

通道间内容传输:

/**
* channel 间的传输
*
* @param sFileName 源文件
* @param dFileName 目标文件
*/
public static void channelToChannel(String sFileName, String dFileName) {
  try {
	  RandomAccessFile sAccess = new RandomAccessFile(sFileName, "rw");
	  RandomAccessFile dAccess = new RandomAccessFile(dFileName, "rw");
	  FileChannel sChannel = sAccess.getChannel();
	  FileChannel dChannel = dAccess.getChannel();

	  long pos = 0;
	  long sCount = sChannel.size();
	  long dCount = dChannel.size();
//    dChannel.transferFrom(sChannel,pos,sCount);//dChannel 必须是FileChannel
	  sChannel.transferTo(pos, dCount, dChannel);///sChannel 是FileChannel
  } catch (FileNotFoundException e) {
	  e.printStackTrace();
  } catch (IOException e) {
	  e.printStackTrace();
  }
}

TCP通信 SocketChannel

基本的C/S TCP通信

Client客户端写法:

/**
* Client SocketChannel 写法:
*/
public static void client(String fileName) {
    SocketChannel sc = null;
    try {
    // 创建一个SocketChannel 通道
    ////TODO: FileChannel无法设置为非阻塞模式,它总是运行在阻塞模式下
    sc = SocketChannel.open();
    ///TODO:非阻塞IO状态下,socketChannel就可以异步地执行read()、write()、connect()方法了
    sc.configureBlocking(false);
    sc.connect(new InetSocketAddress("http://jianshu.com", 80));
        while (!sc.finishConnect()) {///保证在connect成功之前,可以做别的事情
            //做点别的事。。。。。
        }
        while((int len  = sc.read(xxx))==0){ ///保证NBIO下,read数据不会read空
             // 做别的事。。。
        }
        while((int len  = sc.write(xxx))==0){///保证NBIO下,write数据不会write空
            // 做别的事。。。
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (sc != null) {
                sc.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Server服务端写法:

/**
* 关于:ServerSocketChannel
*/
public static void serverSocketChannel() {
    ServerSocketChannel serverSocketChannel = null;
    try {
        ///打开
        serverSocketChannel = ServerSocketChannel.open();
        ///连接并开始监听TCP 9999端口
        serverSocketChannel.socket().bind(new InetSocketAddress(9999));
        ///TODO:可设置非阻塞状态(需要检查accept到的socketChannel是否为null)
        serverSocketChannel.configureBlocking(false);
        while (true) {
            SocketChannel socketChannel = serverSocketChannel.accept();
            //TODO: 非阻塞时需要考虑返回的socketChannel对象是否为null
            if(socketChannel != null){
            //do something with socketChannel...
            }
            //do something with socketChannel...
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (serverSocketChannel != null)
            try {
                serverSocketChannel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
    }
}

配合Selector,简化SocketChannel在非阻塞IO状态下的Null情况监测逻辑

/**
* 关于 选择器 和 SocketChannel 的配合使用
*/
public static void selectorAndSocketChannel(String fileName) {
  SocketChannel sc1 = null;
  SocketChannel sc2 = null;
  SocketChannel sc3 = null;
  try {
      // 创建几个SocketChannel 通道
      ////TODO: FileChannel无法设置为非阻塞模式,它总是运行在阻塞模式下
      sc1 = SocketChannel.open();
      sc2 = SocketChannel.open();
      sc3 = SocketChannel.open();
      ///TODO:非阻塞IO状态下,socketChannel就可以异步地执行read()、write()、connect()方法了
      sc1.configureBlocking(false);
      sc2.configureBlocking(false);
      sc3.configureBlocking(false);
      sc1.connect(new InetSocketAddress("http://jenkov.com", 80));
      sc2.connect(new InetSocketAddress("http://jenkov.com", 80));
      sc3.connect(new InetSocketAddress("http://jenkov.com", 80));

      // 创建Selector
      Selector selector = Selector.open();
      // 注册channels
      SelectionKey key1 = sc1.register(selector, SelectionKey.OP_READ);
      SelectionKey key2 = sc2.register(selector, SelectionKey.OP_READ);
      SelectionKey key3 = sc3.register(selector, SelectionKey.OP_READ);
      // 持续监控selector的四个事件(接受、连接、读、写)是否就绪
      while (true) {
          int readyChannels = selector.select();
          if (readyChannels == 0) continue;
          Set selectedKeys = selector.selectedKeys();
          Iterator keyIterator = selectedKeys.iterator();
          while (keyIterator.hasNext()) {
              SelectionKey key = (SelectionKey) keyIterator.next();
              if (key.isAcceptable()) {
                  // a connection was accepted by a ServerSocketChannel.
                  ///我的这个连接请求被服务端接受了
              } else if (key.isConnectable()) {
                  // a connection was established with a remote server.
                  ///已经连接上
              } else if (key.isReadable()) {
                  // a channel is ready for reading
                  ///可读数据
              } else if (key.isWritable()) {
                  // a channel is ready for writing
                  ///可写数据
              }
          }
          keyIterator.remove();
      }

  } catch (IOException e) {
      e.printStackTrace();
  } finally {
      try {
          if (sc1 != null) {
              sc1.close();
          }
          if (sc2 != null) {
              sc2.close();
          }
          if (sc3 != null) {
              sc3.close();
          }
      } catch (IOException e) {
          e.printStackTrace();
      }
  }
}

UDP通信 DatagramChannel

收发UDP数据包 的简单示例:

/**
* 关于:DatagramChannel
* UDP 无连接网络协议
* 发送和接收的是数据包
*/
public static void datagramChannel() {
  DatagramChannel datagramChannel = null;
  try {
	  ///打开
	  datagramChannel = DatagramChannel.open();
	  ///连接并开始监听UDP 9999端口
	  datagramChannel.socket().bind(new InetSocketAddress(9999));
	  // 接收数据包(receive()方法会将接收到的数据包内容复制到指定的Buffer. 如果Buffer容不下收到的数据,多出的数据将被丢弃。 )
	  ByteBuffer buf = ByteBuffer.allocate(48);
	  buf.clear();
	  datagramChannel.receive(buf);
	  // 发送数据 send()
	  String sendMsg = "要发送的数据";
	  ByteBuffer sendBuf = ByteBuffer.allocate(48);
	  sendBuf.clear();
	  sendBuf.put(sendMsg.getBytes());
	  sendBuf.flip();
	  datagramChannel.send(sendBuf,new InetSocketAddress("xxxxx",80));

	  // TODO: 连接到特定的地址(锁住DatagramChannel ,让其只能从特定地址收发数据 因为UDP无连接,本身没有真正的连接产出)
	  datagramChannel.connect(new InetSocketAddress("jenkov.com", 80));
	  ///连接后,也可以使用Channal 的read()和write()方法,就像在用传统的通道一样。只是在数据传送方面没有任何保证

  } catch (IOException e) {
	  e.printStackTrace();
  } finally {
	  if (datagramChannel != null)
		  try {
			  datagramChannel.close();
		  } catch (IOException e) {
			  e.printStackTrace();
		  }
  }
}

NIO管道(Pipe)

首先,什么是NIO管道,下图可以看出其内部结构和功能特点:

  • NIO Pipe,是两个线程之间的单向连接通道
  • Pipe类内部有两个成员属性,分别是:
    • Pipe.SinkChannel:数据入口通道
    • Pipe.SourceChannel:数据出口通道
  • 整体原理:ThreadA中获取的数据通过SinkChannel传入(写入)管道,当ThreadB要读取ThreadA的数据,则通过管道的SourceChannel传出(读取)数据。

示例:管道传输数据

/**
* 关于NIO管道(Pipe)
* 定义:2个线程之间的单向数据连接
*/
public static void aboutPipe(){
  Pipe pipe=null;
  try {
      /// 打开管道
      pipe = Pipe.open();
      ///TODO: 一、 向管道写入数据
      /// 访问Pipe.sinkChannel,向Pipe写入数据
      /// 首先,获取Pipe.sinkChannel
      Pipe.SinkChannel sinkChannel = pipe.sink();
      /// 然后,调用write(),开始写入数据
      String newData = "New String to write to file..." + System.currentTimeMillis();
      ByteBuffer buf = ByteBuffer.allocate(48);
      buf.clear();
      buf.put(newData.getBytes());
      buf.flip();
      while(buf.hasRemaining()){
      sinkChannel.write(buf);
      }
      // TODO: 二、读取管道中的数据
      // 首先,获取Pipe.sourceChannel
      Pipe.SourceChannel sourceChannel = pipe.source();
      /// 读取数据到buffer
      ByteBuffer buf2 = ByteBuffer.allocate(48);
      int bytesRead = sourceChannel.read(buf2);
  } catch (IOException e) {
      e.printStackTrace();
  }
}

字符集 Charset

编码:字符串->字节数组。解码:字节数组->字符串

查看Charset里都有哪些编码:

Map<String, Charset> map = Charset.availableCharsets();
map.forEach((k,v)->{
    System.out.println(k);//常见的UTF-8等等..
});

缓冲区编解码:

Charset c = Charset.forName("UTF-8");
CharsetEncoder e = cs1.newEncoder();  //获取编码器
CharsetDecoder d = cs1.newDecoder();  //获取解码器    
CharBuffer buf = CharBuffer.allocate(1024);
buf.put("二狗子到此一游");
buf.flip();
ByteBuffer bBuf = e.encode(buf );//编码
bBuf.flip();                     //解码
CharBuffer buf2= d.decode(bBuf);
System.out.println(buf2.toString());

直接与非直接缓冲区

非直接缓冲区:通过 allocate() 方法分配缓冲区,将缓冲区建立在 JVM 的内存中。
直接缓冲区:通过 allocateDirect() 方法分配直接缓冲区,将缓冲区建立在物理内存中。可以提高效率。

非直接缓冲区

我们之前说过NIO通过通道连接磁盘文件与应用程序,通过缓冲区存取数据进行双向的数据传输。物理磁盘的存取是操作系统进行管理的,与物理磁盘的数据操作需要经过内核地址空间;而我们的Java应用程序是通过JVM分配的缓冲空间。数据需要在内核地址空间和用户地址空间,在操作系统和JVM之间进行数据的来回拷贝,无形中增加的中间环节使得效率与后面要提的之间缓冲区相比偏低。

直接缓冲区

直接缓冲区则不再通过内核地址空间和用户地址空间的缓存数据的复制传递,而是在物理内存中申请了一块空间,这块空间映射到内核地址空间和用户地址空间,应用程序与磁盘之间的数据存取之间通过这块直接申请的物理内存进行。

直接和非直接缓冲区的要点

字节缓冲区要么是直接的,要么是非直接的。如果为直接字节缓冲区,则 Java 虚拟机会尽最大努力直接在此缓冲区上执行本机 I/O 操作。也就是说,在每次调用操作系统基础的一个本机 I/O 操作之前(或之后),虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。

直接字节缓冲区可以通过调用此类的 allocateDirect() 工厂方法来创建。此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在 直接缓冲区能在程序性能方面带来明显好处时 分配它们。

直接字节缓冲区还可以通过 FileChannel 的 map() 方法 将文件区域直接映射到内存中来创建。该方法返回MappedByteBuffer 。 Java 平台的实现有助于通过 JNI 从本机代码创建直接字节缓冲区。如果以上这些缓冲区中的某个缓冲区实例指的是不可访问的内存区域,则试图访问该区域不会更改该缓冲区的内容,并且将会在访问期间或稍后的某个时间导致抛出不确定的异常。

字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其 isDirect() 方法来确定。提供此方法是为了能够在性能关键型代码中执行显式缓冲区管理。

那么既然直接缓冲区的性能更高、效率更快,为什么还要存在两种缓冲区呢?因为直接缓冲区也存在着一些缺点:

  • 不安全;
  • 消耗更多,因为它不是在JVM中直接开辟空间。这部分内存的回收只能依赖于垃圾回收机制,垃圾什么时候回收不受我们控制;
  • 数据写入物理内存缓冲区中,程序就失去了对这些数据的管理,即什么时候这些数据被最终写入从磁盘只能由操作系统来决定,应用程序无法再干涉。

注意:直接缓冲区适合与数据长时间存在于内存,或者大数据量的操作时更加适合。

两种操作方式

  • 使用ByteBuffer.allocateDirect(1024);进行分配
  • 建立内存映射文件,来建立直接缓冲区

内存映射文件

MappedByteBuffer是java nio引入的文件内存映射方案,读写性能极高。

MappedByteBuffer是ByteBuffer 的子类。你可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行映射。  

映射类型有3种:只读,读定,写拷贝。

try {
   File file = new File("filename");

   // 创建一个只读的内存映射文件
   FileChannel roChannel = new RandomAccessFile(file, "r").getChannel();
   ByteBuffer roBuf = roChannel.map(FileChannel.MapMode.READ_ONLY, 0, (int)roChannel.size());

   // 创建一个可读写的内存映射文件
   FileChannel rwChannel = new RandomAccessFile(file, "rw").getChannel();
   ByteBuffer wrBuf = rwChannel.map(FileChannel.MapMode.READ_WRITE, 0, (int)rwChannel.size());

   // 创建一个可读写的文件映射副本(写拷贝) ,任何写操作只对针对副本
   ByteBuffer pvBuf = roChannel.map(FileChannel.MapMode.READ_WRITE, 0, (int)rwChannel.size());
} catch (IOException e) {

}

注意:对可读写映射文件的修改并不是同步的,即对映射文件的修改不会立即发送到底层存储设备,若要同步使用MappedByteBuffer.force() 来强制发送。

 

posted @ 2022-01-23 20:05  残城碎梦  阅读(272)  评论(0编辑  收藏  举报