深入java NIO系列之通路分析与源码解读(二)
深入java NIO系列之通道分析与源码解读(二)
-
通道概念
通道用于在字节缓冲区和通道另一侧的实体之间有效进行数据传输。通道与操作系统的文件描述符和文件句柄有着一一对应的关系借助通道可以用最小的开销来访问操作系统本身的IO服务,缓冲区则是通道内部用来发送和接收数据的端点。通道是访问IO服务的导管,IO可以分为两种:文件(File)IO和流(Stream)IO,那么通道也可以分为:文件通道和套接字通道
- 通道的创建
一个FileChannel对象只能通过一个RandomAccessFile,FileInputStream,FileOutputStream的getChannel()方法获取,不能直接创建
- 使用通道
通道可以是单向的也可以是双向的,一个通道类可以实现ReadableByteChannel接口的read()方法,也可以实现WriteableByteChannel接口的write()方法,只要实现其中任一方法,通道就是单向的,实现两个接口为双向。
一个文件可以通过不同的权限打开,从FileInputStream对象的getChannel()方法获取的FileChannel对象是只读的,尽管FileChannel实现了ByteChannel接口,但是在该通道上调用write()方法时,仍会报出NonWriteableChannelException,因为FileInputStream对象总是以read-only权限打开文件。
public void channelcopy() throws IOException { ReadableByteChannel source = Channels.newChannel(System.in); WritableByteChannel dst = Channels.newChannel(System.out); ByteBuffer buffer = ByteBuffer.allocate(1024); while(source.read(buffer)!=-1) { buffer.flip(); dst.write(buffer); buffer.compact(); } buffer.flip(); while(buffer.hasRemaining()) { dst.write(buffer); } }
通道可以通过阻塞和非阻塞的方式运行,只有非阻塞的通道不会让线程阻塞,因为请求的操作要么立即完成,要么返回结果表明未进行操作,只有面向流的操作才进行非阻塞方式。
- 关闭通道
调用通道的close()方法时,可能会导致在通道关闭底层IO服务时线程暂时阻塞,可以通过isOpen()方法测试通道的开放状态如果通道实现InterruptibleChannel接口,那么如果一个线程在该通道上被阻塞或者被中断,通道被关闭,阻塞线程会产生一个CloseByInterruptException,我们可以通过线程的isInterrupted()方法来测试当前线程的中断状态,当一个通道被关闭,休眠在该通道上的所有线程豆浆杯唤醒,并接受到一个AsynchronousCloseException异常
- 通道的发散和聚集
通过Scatter或者gather可以在多个缓冲区上实现一个简单的IO操作对于write操作,数据是从几个缓冲区按照顺序抽取并沿着通道发送的,缓冲区自身不具备gather的能力。对于read操作,从通道读取的操作会按照顺序被散步到多个缓冲区,将缓冲区填满当在通道上发起一个scatter或者gather请求时,请求会被翻译成适当的本地调用,来直接填充或抽取缓冲区,因为减少了缓冲区的拷贝和系统调用
/** * * header和body首先被封装成数组作为通道read()方法的参数, * 通道按照buffer数组中的顺序进行填充,当一个buffer被填满之后填充下一个buffer * Scattering Reads在移动下一个buffer前,必须填满当前的buffer, * 这也意味着它不适用于动态消息 * */ public void scatter() { ByteBuffer header = ByteBuffer.allocate(1024); ByteBuffer body = ByteBuffer.allocate(1024); ByteBuffer[] bufferArray = {header,body}; channel.read(bufferArray); } /** * * write()方法会根据buffer数组中的顺序将数据写入通道中,write()写入的数据位buffer中position和limit之间的数据, * 与scatter相反,write()可以很好的处理动态数据 * */ public void gather() { ByteBuffer header = ByteBuffer.allocate(1024); ByteBuffer body = ByteBuffer.allocate(1024); //对两个缓冲区进行数据填充 header.put("hello world".getBytes()); body.put("hello world".getBytes()); ByteBuffer[] bufferArray = {header,body}; channel.write(bufferArray); }
- 文件通道
文件通道都是阻塞式的,因为操作系统复杂的缓存和预取机制,使得本地磁盘的IO操作延迟很少,一个进程可以从操作系统请求一个或多个IO操作,和其他通道一样,只要可能,FileChannel都会尝试本地IO服务FileChannel是线程安全的,多个线程可以在同一个实例上并发调用,但是影响通道位置和通道大小的操作是单线程的,当有线程视图修改通道位置或者大小时,其他线程阻塞每个FileChannel对象都同一个文件描述符有着一对一的关系,同底层的文件描述符一样,每个FileChannel都有一个file-position,决定接下来文件的哪一处数据将会被读或者写。如果将position的位置定位超出当前文件的大小,并在此时调用write(),则将有可能造成"文件空洞",如果调用write方法时position前进超出了文件大小的值,文件会扩展以容纳更多的字节force() 告诉通道强制将所有待定的修改更新到磁盘(操作系统会缓存数据和延迟磁盘更新以提高性能)force()方法的参数表示在方法返回值前文件的元数据是否也要同步更新到磁盘,元数据指文件所有者、访问权限、最后修改时间等
- 文件锁定
并非所有的操作系统和文件系统都支持共享文件锁,对于不支持的请求会自动升级为独占锁的请求,在不同的操作系统不,甚至是在同一操作系统的的不同文件系统上,文件锁定的语义都可能有所差异FileChannel实现的文件锁定,锁的对象是文件,不是通道或者线程,如果一个线程获得该文件的独占所,然后第二个线程利用一个单独打开的通道要求该文件的独占锁,那么第二个线程的请求会被批准。文件锁旨在进程级上判优文件访问/** * *获取对此通道文件的独占锁 *某些操作系统不支持共享锁定,在这种情况下,自动将对共享锁定的请求转换为对独占锁定的请求。 *可通过调用所得锁定对象的 isShared 方法来测试新获取的锁定是共享的还是独占的。 * * ClosedChannelException - 如果此通道已关闭 * AsynchronousCloseException - 如果调用线程阻塞于此方法中时另一个线程关闭了此通道 * FileLockInterruptionException - 如果调用线程阻塞于此方法中时被中断 * OverlappingFileLockException - 如果此 Java 虚拟机已经持有与所请求区域重叠的锁定,或者如果另一个线程已阻塞在此方法中并且正在试图锁定同一文件的重叠区域 * NonWritableChannelException - 如果没有为写入打开此通道 * IOException - 如果发生其他 I/O 错误 * */ public void lock() throws IOException { File file = new File("e://1.txt"); FileInputStream in = new FileInputStream(file); FileChannel fc = in.getChannel(); FileLock lock = fc.lock();//获取独占锁 lock.isShared();//测试新获取的锁定是共享的还是独占的 lock.isValid(); } /** * trylock()与lock()方法的不同在于trylock会尝试去获取独占锁, * 如果当前文件的独占锁已经被占有,那么该方法不会导致线程阻塞,会立即返回null * @throws IOException */ public void trylock() throws IOException { File file = new File("e://1.txt"); FileInputStream in = new FileInputStream(file); FileChannel fc = in.getChannel(); FileLock lock = fc.tryLock();//尝试获取独占锁 lock.isShared();//测试新获取的锁定是共享的还是独占的 } /** * 锁定文件的某一个区域 * * 参数解释: * position - 锁定区域开始的位置;必须为非负数 * size - 锁定区域的大小;必须为非负数,并且 position + size 的和必须为非负数 * shared - 要请求共享锁定,则为 true,在这种情况下此通道必须允许进行读取(可能是写入)操作; * 要请求独占锁定,则为 false,在这种情况下此通道必须允许进行写入(可能是读取)操作 * */ public void lockSome() throws IOException { File file = new File("e://1.txt"); FileInputStream in = new FileInputStream(file); FileChannel fc = in.getChannel(); FileLock lock = fc.lock(0,1024,false);//获取独占锁 lock.isShared();//测试新获取的锁定是共享的还是独占的 }FileLock对象还有一些其他的方法,overlaps().isShared()等
- 内存映射文件
FileChannel类提供一个map()方法,该方法可以在一个打开的文件和MappedByteBuffer建立一个虚拟内存映射,MappedByteBuffer的行为多数方面类似一个内存缓冲区,不过该对象的数据存储再一个磁盘的文件中。通过调用get()方法会从磁盘中获取数据,对映射的缓冲区调用put()方法会更新磁盘上的那个文件通过内存映射机制访问一个文件会比常规读写高效,因为不需要做明确的系统调用,并且操作系统的虚拟内存页会自动缓存内存页,这些页是有系统内存来缓存,不需要消耗jvm的内存当我们为一个文件建立虚拟内存映射之后,文件数据通常不会因此被从磁盘读取到内存(这取决于操作系统)。该过程类似打开一个文件:文件先被定位,然后一个文件句柄会被创建,当您准备好之后就可以通过这个句柄来访问文件数据。对于映射缓冲区,虚拟内存系统将根据您的需要来 把文件中相应区块的数据读进来。这个页验证或防错过程需要一定的时间,因为将文件数据读取到内存需要一次或多次的磁盘访问。某些场景下,您可能想先把所有的页都读进内存以实现最小的缓冲区访问延迟。如果文件的所有页都是常驻内存的,那么它的访问速度就和访问一个基于内存的缓冲区一样了。load( )方法会加载整个文件以使它常驻内存。正如我们在第一章所讨论的,一个内存映射缓冲区会建立与某个文件的虚拟内存映射。此映射使得操作系统的底层虚拟内存子系统可以根据需要将文件中相应区块的数据读进内存。已经在内存中或通过验证的页会占用实际内存空间,并且在它们被读进RAM时会挤出最近较少使用的其他内存页。在一个映射缓冲区上调用load( )方法会是一个代价高的操作,因为它会导致大量的页调入(page-in),具体数量取决于文件中被映射区域的实际大小。然而,load( )方法返回并不能保证文件就会完全常驻内存,这是由于请求页面调入(demand paging)是动态的。具体结果会因某些因素而有所差异,这些因素包括:操作系统、文件系统,可用Java虚拟机内存,最大Java虚拟机内存,垃圾收集器实现过程等等。请小心使用load( )方法,它可能会导致您不希望出现的结果。该方法的主要作用是为提前加载文件埋单,以便后续的访问速度可以尽可能的快。对于那些要求近乎实时访问(near-realtime access)的程序,解决方案就是预加载。但是请记住,不能保证全部页都会常驻内存,不管怎样,之后可能还会有页调入发生与锁的机制不一样的是,MappedByteBuffer 没有unmap()方法,映射缓冲区没有绑定到创建他们的通道上,关闭相关联的FileChannel不会破坏映射,只有丢弃缓冲区对象本身才会破坏映射。所有的MappedByteBuffer对象占用的内存都位于jvm内存堆之外/** * * mode - 根据是按只读、读取/写入或专用(写入时拷贝)来映射文件, *分别为 FileChannel.MapMode 类中所定义的 READ_ONLY、READ_WRITE 或 PRIVATE 之一 * position - 文件中的位置,映射区域从此位置开始;必须为非负数 * size - 要映射的区域大小;必须为非负数且不大于 Integer.MAX_VALUE * * MapMode.READ_ONLY(只读): 试图修改得到的缓冲区将导致抛出 ReadOnlyBufferException。 * MapMode.READ_WRITE(读/写): 对得到的缓冲区的更改最终将传播到文件;该更改对映射到同一文件的其他程序不一定是可见的。 * MapMode.PRIVATE(专用): 对得到的缓冲区的更改不会传播到文件,并且该更改对映射到同一文件的其他程序也不是可见的; *相反,会创建缓冲区已修改部分的专用副本。 * * 对于只读映射关系,此通道必须可以进行读取操作;对于读取/写入或专用映射关系,此通道必须可以进行读取和写入操作。 * * 映射关系一经创建,就不再依赖于创建它时所用的文件通道。特别是关闭该通道对映射关系的有效性没有任何影响。 */ public static void map() throws IOException { long begin = System.currentTimeMillis(); File file = new File("e://1.txt"); FileInputStream in = new FileInputStream(file); FileChannel fc = in.getChannel(); MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size()); mbb.load();//将缓冲区中的内容加载到物理内存中 System.out.println(mbb.position()+" "+mbb.limit()); byte[] b = new byte[1024]; mbb.get(b); System.out.println(new String(b)); fc.close(); }
- channel-to-channel 传输
FileChannel 添加了transferto和transferfrom方法,保证文件数据可以从一个位置批量传输到另一个位置transferTo() 和transferFrom()方法允许将一个通道的交叉连接到另一个通道,而不需要中间缓冲区来传递数据/** * * 将字节从此通道的文件写入到指定的可写入文件中 * public abstract long transferTo(long position, long count,WritableByteChannel target) * position:此通道的位置 * count:此通道读取的字节数量 * target:指定写入目标通道 */ public static void transferFrom() throws IOException { File sourceFile = new File("e://1.txt"); File targetFile = new File("e://4.txt"); FileChannel source = new FileInputStream(sourceFile).getChannel(); FileChannel target = new FileOutputStream(targetFile).getChannel(); target.position(target.size()); source.transferTo(0, source.size(), target); source.close(); target.close(); }