一、介绍
NIO是一个双向的缓存通道,通道负责建立和缓冲区的链接。
Channel负责传输,Buffer负责中转储存。
二、要素
1. 缓存区(Buffer)
1.1 介绍
缓存区(Buffer):一个用于特定基本数据类型的容器。由java.nio包定义的,所有缓冲区都是Buffer抽象类的子类。
Java NIO中的Buffer主要用于与NIO通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中。
Buffer就像一个数组,可以保存多个相同类型的数据。根据数据类型不同(boolean除外),有以下Buffer常用子类:
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
上述Buffer类都是采用相似的方法进行管理数据,只是各自管理的数据类型不同而已。都是通过如下方法获取一个Buffer对象:
static XXXBufer allocate(int capacity)
:创建一个容量为capacity的XXXBuffer对象。
1.2 基本属性
容量(capacity):表示Buffer最大数据容量,缓冲区容量不能为负,并且创建后不能更改。
限制(limit):第一个不应该读取或写入的数据的索引,即位于limit后的数据不可读写。缓存区的限制不能为负,并且不能大于其容量。
位置(position):下一个要读取或写入的数据的索引。缓冲区的位置不能为负,并且不能大于其容量。
标记(mark)与重置(reset):标记是一个索引,通过Buffer中的mark()方法指定Buffer中一个特定的position,之后可以通过reset()方法恢复到这个position。
遵守原则:0<=mark<=position<=limit<=capacity,管道将会从Buffer中读取position到limit的内容。
1.3 常用方法
方法 | 描述 |
---|---|
Buffer clear() |
清空缓冲区并返回对缓冲区的引用 |
Buffer flip() |
将缓冲区的界限设置为当前位置,并将当前位置重置为0 |
int capacity() |
返回Buffer的capacity的大小 |
boolean hasRemaining() |
判断缓冲区中是否还有元素 |
int limit() |
返回Buffer的界限(limit)的位置 |
Buffer limit(int newLimit) |
将设置缓冲区界限为newLimit,并返回一个具有新limit的缓冲区对象 |
Buffer mark() |
对缓冲区设置标记 |
int position() |
返回缓冲区的当前位置position |
Buffer position(int newPosition) |
将设置缓冲区的当前位置为newPosition,并返回修改后的Buffer对象 |
int remaining() |
返回position和limit之间的元素个数 |
Buffer reset() |
将位置position转到以前设置的mark所在的位置 |
Buffer rewind() |
将位置设置为0,取消设置mark |
1.4 缓冲区的数据操作
Buffer所有子类提供了两个用于数据操作的方法:get()与put()方法
获取Buffer中的数据
get()
:读取单个字节get(int index)
:读取指定索引位置的字节(不会移动position)get(byte[] dst, int offset, int length)
:get(byte[] dst)
:批量读取多个字节到dst中放入数据到Buffer
put(byte b)
:将给定单个字节写入缓冲区的当前位置put(byte[] src, int offset, int length)
:put(byte[] src)
:将src中的字节写入缓冲区的当前位置put(ByteBuffer src)
:put(int index, byte b)
:将指定字节写入缓冲区的索引位置(不会移动position)
1 | /** |
1.5 直接缓冲区与非直接缓冲区
字节缓冲区要么是直接的,要么是非直接的。如果为直接字节缓冲区,则Java虚拟机会尽最大努力直接在此缓冲区上执行本机I/O操作。也就是说,在每次调用基础操作系统的一个本机I/O操作之前(或之后),虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。
直接字节缓冲区可以通过调用此类的
allocateDirect()
工厂方法来创建。此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的本机I/O操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处是分配它们。直接字节缓冲区还可以通过FileChannel的map()方法将文件区域直接映射到内存中来创建。该方法返回MappedByteBuffer。Java平台的实现有助于通过JNI从本机代码创建直接字节缓冲区。如果以上这些缓冲区中的某个缓冲区实例指的是不可访问的内存区域,则试图访问该区域不会更改该缓冲区的内容,并且将会在访问期间或稍后的某个时间导致抛出不确定的异常。
字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其isDirect()方法来确定。提供此方法是为了能够在性能关键型代码中执行显示缓冲区管理。
2. 通道(Channel)
由java.nio.channels
包定义。Channel表示IO源与目标打开的连接。Channel类似于传统的“流”。只不过Channel本身不能直接访问数据,Channel只能与Buffer进行交互。
2.1 主要实现类
2.1.1 FileChannel
用于读取、写入、映射和操作文件的通道
- FileChannel的常用方法
方法 | 描述 |
---|---|
int read(ByteBuffer dst) |
从Channel中读取数据到ByteBuffer |
long read(ByteBuffer[] dsts, int offset, int length) |
将Channel中的数据“分散”到ByteBuffer[] |
int write(ByteBuffer src) |
将ByteBuffer中的数据写入到Channel |
long write(ByteBuffer[] srcs, int offset, int length) |
将ByteBuffer[]中的数据“聚集”到Channel |
long position() |
返回此通道的文件位置 |
FileChannel position(long newPosition) |
设置此通道的文件位置 |
long size() |
返回此通道的文件的当前大小 |
FileChannel truncate(long size) |
将此通道的文件截取为给定大小 |
void force(boolean metaData) |
强制将所有对此通道的文件更新写入到储存设备中 |
2.1.2 DatagramChannel
用过UDP读写网络中的数据通道
操作步骤:打开DatagramChannel -> 接收/发送数据
1 | public class TestNonBlockingNIO2 { |
2.1.3 SocketChannel
Java NIO中的SocketChannel是一个连接到TCP网络套接字的通道。通过TCP读写网络中的数据
操作步骤:打开SocketChannel -> 读写数据 -> 关闭SocketChannel
1 | public void client() throws IOException { |
2.1.4 ServerSocketChannel
Java NIO中的ServerSocketChannel是一个可以监听新进来的TCP连接的通道,就像标准IO中的ServerSocket一样。可以监听新进来的TCP连接,对每一个新进来的连接都会创建一个SocketChannel。
1 | public void server() throws IOException{ |
2.2 获取通道
获取通道的一种方式是对支持通道的对象调用getChannel()
方法。支持通道的类如下:
FileInputStream
FileOutputStream
RandomAccessFile
DatagramSocket
Socket
ServerSocket
获取通道的其他方式是使用Files类的静态方法newByteChannel()
获取字节通道。或者通过通道的静态方法open()
打开并返回指定通道。FileChannel.open(Path path, OpenOption... options)
2.3 通道之间的数据传输
2.3.1 缓冲区传输
- 将Buffer中数据写入Channel
1 | // 将Buffer中数据写入Channel中 |
- 从Channel读取数据到Buffer
1 | // 从Channel读取数据到Buffer中 |
- 使用示例
1 | public class TestChannel { |
1 | //2.使用直接缓冲区完成文件的复制(内存映射文件的方式) |
2.3.2 直接传输
transferFrom()
transferTo()
1 | RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw"); |
1 | /** |
2.4 通道之间的内存映射
Java IO操作中通常采用BufferedReader,BufferedInputStream等带缓冲的IO类处理大文件,不过Java NIO中引入了一种基于MappedByteBuffer操作大文件的方式,其读写性能极高
1 | public void test2() throws IOException { |
3. 分散(Scatter)和聚集(Gather)
分散读取(Scattering Reads)是指从Channel中读取的数据“分散”到多个Buffer中。
聚集写入(Gathering Writes)是指将多个Buffer中的数据“聚集”到Channel。
1 | //分散和聚集 |
4. 阻塞与非阻塞
- 传统的IO流都是阻塞式的。也就是说,当一个线程调用read()或write()时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务。因此,在完成网络通信进行IO操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量客户端时,性能急剧下降。
- Java NIO是非阻塞模式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞IO的空闲时间用于在其他通道上执行IO操作,所以单独的线程可以管理多个输入和输出通道。因此,NIO可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。
5. 选择器(Selector)
选择器(Selector)是SelectableChannel对象的多路复用器,Selector可以同时监控多个SelectableChannel的IO状况,也就是说,利用Selector可使一个单独的线程管理多个Channel。Selector是非阻塞IO的核心。
5.1 选择器的使用
- 创建Selector:通过调用
Selector.open()
方法创建一个Selector
1 | //创建一个选择器,并把SocketChannel交给selector对象 |
- 向选择器注册通道
1 | // 创建一个Socket套接字 |
当调用register(Selector sel, int ops)
给通道注册选择器时,需要设置选择监听的事件类型,通过第二个参数ops指定。
可以监听的事件类型(可使用SelectionKey的四个常量表示):
读:SelectionKey.OP_READ(1 << 0 : 1)
写:SelectionKey.OP_WRITE(1 << 2 : 4)
连接:SelectionKey.OP_CONNECT(1 << 3 : 8)
接受:SelectionKey.OP_ACCEPT(1<<4 : 16)
若注册时不止监听一个事件,则可以使用“位或”操作符连接。
1 | int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE; |
5.2 选择键(SelectionKey)
表示SelectableChannel和Selector之间的注册关系。
方法 | 描述 |
---|---|
int interestOps() | 获取感兴趣事件集合 |
int readyOps() | 获取通道已经准备就绪的操作的集合 |
SelectableChannel channel() | 获取注册通道 |
Selector selector() | 返回选择器 |
boolean isReadable() | 检测Channel中读事件是否就绪 |
boolean isWritable() | 检测Channel中写事件是否就绪 |
boolean isConnectable() | 检测Channel中连接是否就绪 |
boolean isConnectable() | 检测Channel中接收是否就绪 |
Object attach(Object ob) | 将给定的对象附加到此键 |
Object attachment() | 获取当前的附加对象 |
1 | selectionKey.attach(theObject); |
5.3 选择器(Selector)的常用方法
方法 | 描述 |
---|---|
Set<SelectionKey> keys() |
所有的SelectionKey集合。代表注册在该Selector上的Channel |
Set<SelectionKey> selectedKeys() |
被选择的SelectionKey集合。返回次Selector的已选择键集 |
int select() |
监控所有注册的Channel,当它们中间有需要处理的IO操作时,根据设置SelectionKey的集合,返回符合匹配的Channel的数量 |
int select(long timeout) |
可以设置超时时长的select()操作 |
int selectNow() |
执行一个立即返回的select()操作,该方法不会阻塞线程 |
Selector wakeup() |
使一个还未返回的select()操作方法立即返回 |
void close() |
关闭该选择器 |
6. 管道(Pipe)
Java NIO管道是2个线程之间的单向数据连接。Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。
- 向管道写数据
1 | public void write(String str) throws IOException { |
- 从管道读数据
1 | public void read(Pipe pipe) throws IOException { |
7. Path与Paths
java.nio.file.Path
接口代表一个平台无关的平台路径,描述了目录结构中文件的位置Paths
提供get()
方法用来获取Path对象Path get(String first, String... more)
:用于将多个字符串连接成路径Path常用方法:
boolean endsWith(String other)
:判断是否以other路径结束boolean startsWith(String other)
:判断是否以other路径开始boolean isAbsolute()
:判断是否是绝对路径Path getFileName()
:返回与调用Path对象关联的文件名Path getName(int index)
:返回的指定索引位置index的路径名称int getNameCount()
:返回Path根目录后面元素的数量Path getParent()
:返回Path对象包含整个路径,不包含Path对象指定的文件路径Path getRoot()
:返回调用Path对象的根路径Path resolve(String other)
:将相对路径解析为绝对路径Path toAbsolutePath()
:作为绝对路径返回调用Path对象String toString()
:返回调用Path对象的字符串表示形式
8. Files类
java.nio.file.Files用于操作文件或目录的工具类
- Files常用方法:
copy(InputStream in, Path target, CopyOption... options)
:文件的复制
Path createDirectory(Path dir, FileAttribute<?>... attrs)
:创建一个目录
Path createFile(Path path, FileAttribute<?>... attrs)
:创建一个文件
void delete(Path path)
:删除一个文件
Path move(Path source, Path target, CopyOption... options)
:将src移动到target位置
long size(Path path)
:返回指定文件的大小
- 用于判断
boolean exists(Path path, LinkOption... options)
:判断文件是否存在
boolean isDirectory(Path path, LinkOption... options)
:判断是否是目录
boolean isExecutable(Path path)
:判断是否是可执行文件
boolean isHidden(Path path)
:判断是否是隐藏文件
boolean isReadable(Path path)
:判断是否是可读
boolean isWritable(Path path)
:判断是否是可写
boolean notExists(Path path, LinkOption... options)
:判断文件是否不存在
<A extends BasicFileAttributes> A readAttributes(Path path, Class<A> type, LinkOption... options)
:获取与path指定的文件相关联的属性
- 用于操作内容
SeekableByteChannel newByteChannel(Path path, OpenOption... options)
:获取与指定文件的连接,options指定打开方式
DirectoryStream<Path> newDirectoryStream(Path dir)
:打开path指定的目录
InputStream newInputStream(Path path, OpenOption... options)
:获取InputStream对象
OutputStream newOutputStream(Path path, OpenOption... options)
:获取OutputStream对象