Java-IO流(三)-NIO


Java-IO流(三)-NIO

在前面所介绍的输入输出流都是阻塞式的输入、输出,即当数据源中没有数据时,它会阻塞该线程。传统的输入、输出都是通过字节的移动来处理的,就是输入输出系统一次只能处理一个字节,因此效率并不高。从JDK1.4开始,Java改进了IO流体系,提供来一些新功能,被称作NIO。新增的功能类被放在java.nio包及子包下,并且对原java.io包中的很多类都以NIO为基础进行改写,新增满足NIO功能。

NIO采用不同的方式来处理输入/输出,用内存映射文件的方式来处理输入/输出,NIO将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样访问文件,这种方式比传统的方式快。它有如下包:

  • java.nio包:主要各种与Buffer相关的类
  • java.nio.channels包:主要包含与ChannelSelector相关的类
  • java.nio.charset包:字符集相关类
  • java.nio.channels.spi:与Channel相关的服务接口
  • java.nio.charset.spi:包含与字符集相关的服务接口

Channel(通道)和Buffer(缓冲)是新IO中两个核心对象,CHannel是对传统的输入/输出的模拟,在NIO中所有数据都需要通过通道传输;Channel与传统的InputStreamOutputStream区别在于它提供一个map()方法,通过该map()方法可以直接将一块数据映射到内存中。

Buffer可以被理解成一个容器,它本质是一个数组,发送到Channel中都所有对象都必须首先放在Buffer中,而从Channel中读取的数据也必须放到Buffer中。

ChannelBuffer外,NIO还提供了将Unicode字符串映射成字节序列以及逆映射操作的Channel类,也提供了非阻塞式输入/输出的Selector类。

使用Buffer

Buffer是一个抽象类,其最常用的子类是ByteBuffer,它可以在底层字节数组上进行get/set操作。对应其他类型的Buffer类有:CharBufferShortBufferIntBufferLongBufferFloatBuffer等。以上类都没有提供构造器,而是提供static XxxBuffer allocate(int capacity)方法,创建一个容量为capacityXxxBuffer对象。

MappedByteBufferByteBuffer子类,用于表示Channel将磁盘文件的部分或全部内容映射到内存中后得到的结果,相应对象由Channelmap()方法返回。

Buffer中有三个重要的概念:容量(Capacity)、界限(limit)、位置(position)

  • 容量(capacity):缓冲区的容量(capacity)表示最大数据容量,创建后不能改变。
  • 界限(limit):第一个不能被读写的缓冲区位置索引。
  • 位置(position):指明下一个可以被读出或写入的缓冲区位置索引,类似记录指针。

Buffer还有一个可选标记mark,允许直接将position定位到该mark处,并满足:

0<=mark<=position<=limit<=capacity

Buffer的主要作用就是装入数据,然后输出数据。开始时,position为0,limitcapacity,程序可以通过put()方法向Buffer中放入一些数据,每放入一些数据,Buffer的position相应的向后移动一些。当Buffer装入数据结束后,调用Bufferflip()方法,该方法将limit设置为position所在位置,并将position设为0,为输出作准备。输出数据后,Buffer调用clear()方法,将position设为0,将limit设为capacity,这样为装数据做准备。

总结来讲:就是flip()方法为取出数据做好准备,clear()方法为装数据做好准备。此外Buffer的常用方法还有capacity()limit()hasRemaining()等。

除了有移动positionlimitmark的方法外,Buffer的所有子类还提供了两个重要的方法:put()get()方法,用于向Buffer中放入和取出数据,可以单个也可以批量。当用这两个方法访问数据时,分为相对绝对两种:

  • 相对(relative):从Buffer的当前Position处开始读取或写入数据,然后Position值按处理元素个数增加。
  • 绝对(Absolute):直接根据索引向Buffer中读取或写入数据,使用绝对方式访问Buffer的数据,position值不变。

通过allocate()方法创建的对象是普通的Buffer对象,ByteBuffer还提供一个allocateDirect()方法来创建直接Buffer,成本会比普通Buffer创建·成本高,但好处是读取效率更高。

由于只有ByteBuffer提供了allocateDirect()方法,所以只能在ByteBuffer级别上创建直接Buffer。如果需要使用其他类型,则应该将该Buffer转换成其他类型Buffer。直接Buffer更适于长期生存的Buffer,因为创建成本较高。

使用Channel

Channel类似于传统的流对象,但还是有区别。Channel可以直接将指定文件的部分或全部直接映射成Buffer;程序不能直接访问Channel中的数据,读取、写入都不行;Channel只能和Buffer进行交互,也就是说程序要取出数据要通过Buffer,写入数据要还要通过Buffer

Java为Channel接口提供了DatagramChannelFileChannelPipe.SinkChannelServerSocketChannelSocketChannel等实现类,并且有各自相应的功能。所有的Channel都不应该通过构造器来直接创建,而是通过传统的节点InputStreamOutputStreamgetChannel()方法来返回对应的channel,不同的节点流获得的Channel不一样。

Channel中最常用的方法是map()read()write(),其中map()方法用于将Channel对应的部分或全部数据映射成ByteBuffer;而read()write()方法都有一系列重载形式,这些方法用于从Buffer中读取或写入数据。

map(FileChannel.MapMode mode,long position,long size),映射模式有只读、读写等,第二三个参数用于映射数据的范围。在RandomAccessFile中也包含了一个getChannel()方法,RandomAccessFile返回的FileChannel()是只读的还是读写的,取决RandomAccessFile打开文件的模式。

字符集和charset

数据是以字节码的形式储存的,明文字符序列经过编码成二进制序列。Java默认使用Unicode字符集,当读取数据到java程序时,就可能出现乱码。JDK1.4提供了Charset来处理字节序列和字符序列之间的转换关系,该类包含了用于创建解码器和编码器的方法,还提供了获取Charset所支持字符集方法,Charset类是不可变的。Charset类还提供了一个availableCharsets()静态方法来获取当前JDK所支持的所有字符集。

  • GBK:简体中文
  • BIG5:繁体中文
  • ISO-8859-1:ISO拉丁字母表
  • UTF-8:8位UCS转换格式
  • UTF-16BE:16位UCS转换格式,地位地址放高位字节
  • UTF-16:16位UCS转换格式

可以使用SystemgetProperties()方法访问本地系统的文件编码格式,属性名为file.encoding

一旦知道字符集别名,就可以调用CharsetforName()方法来创建对应的Charset对象,forName()的参数是相应字符集的别名。通过对象的newDecoder()newEncoder()方法分别返回CharsetDecoderCharsetEncoder对象,代表解码器和编码器,将字符和字节序列相互转换。

文件锁

使用文件锁可以阻止多个进程同时修改一个文件,在NIO中java提供了FileLock来支持文件锁定功能,在FieChannel中提供的lock()/tryLock()方法可以获得文件锁FileLock对象,从而锁定文件。当lock()试图锁定某个文件时,如果无法得到文件锁,程序将一直阻塞;而tryLock()是直接返回文件锁,否则返回null

  • lock(long position,long size,boolean shared):对文件从position位置开始,长度为size的内容加锁。
  • tryLock(long position,long size,boolean shared):非阻塞式的加锁方法,参数与上面类似。

shared为true时,表明是一个共享锁,它允许多个进程读取该文件,阻止进程获取该文件的排它锁。当为false时,表明是一个排它锁,可提高FileLockisShared()来判断。注意:直接使用上述两个方法获取的都是排它锁,处理完文件后通过FileLockrelease()方法释放文件锁。

NIO.2

Java7对原有的NIO进行了改进,主要有:

  • 提供来全面的文件IO和文件系统访问支持
  • 基于异步的Channel的IO

第一个表现为新增的java.nio.file包及各个子包;第二个表现为在java.nio.channels包下增加多个以Asynchronous开头的Channel接口和类。

早期只能通过File类来访问文件系统,现在引入一个path接口,代表一个与平台无关的平台路径。除此之外,还提供了FilesPaths两个工具类,Files包含了静态的工具方法;Paths包含了两个返回path的静态工厂方法。命名都加上s,代表一个工具类。

使用FileVisitor遍历文件和目录

Files类提供了如下方法来遍历文件和子目录:

  • walkFileTree(Path start,FileVisitor<? super Path> visitor):遍历start路径下的所以文件和子目录

  • walkFileTree(Path start,Set<FileVisitOption> options,int maxDepth,FileVisitor<? super Path> visitor):该方法最多遍历maxDepth深度的文件

使用WatchService监控文件变化

早期是通过一个后台线程每隔一段时间去遍历指定文件目录,如果结果与上一次不同,则发生变化。NIOPath类提供来一个方法来监听文件变化。

register(WatchService watcher,WatchEvent.kind<?> ...events):用watcher监听该path代表的目录下的文件变化,events参数指定要监听那些事件。

获取文件系统的WatchService对象:

WatchService watchservice = FileSystem.getDefault().newWatchService;

接下来使用WatchService的方法获取文件事件:

  • WatchKey poll():获取下一个watchkey,没有返回null。

  • WatchKey poll(long timeout,TimeUnit unit):尝试等待timeout时间去获取下一个WatchKey

  • WatchKey take():获取下一个watchkey,没有就一直等。

公众号:菜鸡干Java


文章作者: 江南骚年
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 江南骚年 !
评论
 上一篇
Java多线程(四)-线程通信 Java多线程(四)-线程通信
前言当线程在系统内运行时,程序无法精准控制线程轮换执行,Java提供了一些机制来保证线程协调运行。 synchronized线程通信相关方法借助Object类提供的wait(),notify(),notifyAll()三个方法(不属于Thr
2021-01-23
下一篇 
Java多线程(二)-控制线程 Java多线程(二)-控制线程
Java线程Thread提供一些工具方便控制线程的执行。 join线程Thread提供了让一个线程等待另一个线程完成的方法——join()方法,当程序调用线程的join()方法时,调用线程(比如主线程)将被阻塞,直到被join()方法加入的
2021-01-20
  目录