zoukankan      html  css  js  c++  java
  • Java NIO读书笔记

    简单介绍

    NIO的作用就是改进程序的性能。由于有时候程序的性能瓶颈不再是CPU,而是IO。这时候NIO就派上用场了。NIO的原理就是尽量利用系统底层的资源来提高效率,比方利用DMA硬件减小CPU负荷,利用操作系统的epoll机制避免线程频繁切换。通过底层资源提高系统的吞吐量。


    缓冲区

    缓冲区就是一个固定大小的一组数据。缓冲区有四个很重要的属性:容量,限制,位置,标记。容量就是一个缓冲区最大能容量的元素数量,限制就是对容量进行逻辑上的限制,位置用于跟踪get或者put方法的位置,标记用于reset函数返回上次固定的位置。


    put()方法用于往缓冲区中存入数据,get()方法用于从缓冲区中读取数据。写入操作详细的API有put(byte)、put(index, byte)、put(byte[])、put(byte[], int start, int length),有单个元素的写入,也有批量的写入。读取操作也一样拥有这四种API。


    flip()用于交换未写入的和写入的数据。也就是将limit设为position,将position设为0。一般先存入一组数据之后,经过翻转,再从中读取原本写入的数据。


    compact()将已经读过的数据进行压缩,将未读过的数据拷贝到缓冲区索引號为0的位置。复制之后,原来的数据不会被擦除。


    mark()方法用于标记,reset()方法用于返回上次标记的位置。rewind()、clear()、flip()都会重置标记,position()、limit()、看情况,假设小于标记时也会重置标记。


    缓冲区之间能够比較。比較一定要同样的类型。比較的根据是缓冲区剩余的内容,与标记、位置、容量、限制等无关。


    创建缓冲区能够有两种方法。一种是创建新的缓冲区,调用xxBuffer.allocate,第二是将现有的数组进行封装,缓冲区写入的数据都会写入到原来的数组中。


    缓冲区是能够复制的。调用duplicate()。复制出来的缓冲区事实上是一个视图。复制出来的缓冲区和原来的缓冲区拥有同样的数据,可是每一个缓冲区都有各自的属性,限制、位置、标记都是独立的。复制的时候也能够取缓冲区的一部分,调用slice()。


    缓冲区还分为big-endian和little-endian。java.nio.ByteOrder能够获取本机的字节顺序。


    另一种缓冲区称之为直接缓冲区,能够通过xxBuffer.allocateDirect获取。直接缓冲区就是操作性能比普通的缓冲区要高。


    ByteBuffer提供了asXXBuffer。比方asShortBuffer、asCharBuffer等。这些缓冲区称之为视图缓冲区。就是将字节缓冲区以第二种行为提供给其它程序。ByteBuffer还提供了getInt、getLong、getDouble等方法,这些方法称之为视图操作,好像就在操作第二种类型的缓冲区。写入操作也是一样,也有视图操作。视图缓冲区和视图操作和字节顺序有关,所以在操作之前先设置字节顺序,默认的是BigEndian。


    Java不支持无符号的数据类型。可是总是有解决的方法的。以下就是一种解决的方法。
    package com.ronsoft.books.nio.buffers;
    
    
    import java.nio.ByteBuffer;
    /**
     * Utility class to get and put unsigned values to a ByteBuffer object.
     * All methods here are static and take a ByteBuffer argument.
     * Since java does not provide unsigned primitive types, each unsigned
     * value read from the buffer is promoted up to the next bigger primitive
     * data type. getUnsignedByte() returns a short, getUnsignedShort() returns
     * an int and getUnsignedInt() returns a long.
     There is no getUnsignedLong()
     * since there is no primitive type to hold the value returned. If needed,
     * methods returning BigInteger could be implemented.
     * Likewise, the put methods take a value larger than the type they will
     * be assigning. putUnsignedByte takes a short argument, etc.
     *
     * @author Ron Hitchens (ron@ronsoft.com)
     */
    public class Unsigned
    {
        public static short getUnsignedByte (ByteBuffer bb)
        {
            return ((short)(bb.get() & 0xff));
        }
        public static void putUnsignedByte (ByteBuffer bb, int value)
        {
            bb.put ((byte)(value & 0xff));
        }
        public static short getUnsignedByte (ByteBuffer bb, int position)
        {
            return ((short)(bb.get (position) & (short)0xff));
        }
        public static void putUnsignedByte (ByteBuffer bb, int position,
                                            int value)
        {
            bb.put (position, (byte)(value & 0xff));
        }
        // ---------------------------------------------------------------
        public static int getUnsignedShort (ByteBuffer bb)
        {
            return (bb.getShort() & 0xffff);
        }
        public static void putUnsignedShort (ByteBuffer bb, int value)
        {
            bb.putShort ((short)(value & 0xffff));
        }
        public static int getUnsignedShort (ByteBuffer bb, int position)
        {
            return (bb.getShort (position) & 0xffff);
        }
        public static void putUnsignedShort (ByteBuffer bb, int position,
                                             int value)
        {
            bb.putShort (position, (short)(value & 0xffff));
        }
        // ---------------------------------------------------------------
        public static long getUnsignedInt (ByteBuffer bb)
        {
            return ((long)bb.getInt() & 0xffffffffL);
        }
        public static void putUnsignedInt (ByteBuffer bb, long value)
        {
            bb.putInt ((int)(value & 0xffffffffL));
        }
        public static long getUnsignedInt (ByteBuffer bb, int position)
        {
            return ((long)bb.getInt (position) & 0xffffffffL);
        }
        public static void putUnsignedInt (ByteBuffer bb, int position,
                                           long value)
        {
            bb.putInt (position, (int)(value & 0xffffffffL));
        }
    }




    最后另一种映射缓冲区,这样的缓冲区一定是直接缓冲区,仅仅能由FileChannel创建。


    通道

    通道和缓冲区不同,每一个操作系统都有不同的实现方式,因此通道的代码一般都是接口或者抽象类。


    通道分为堵塞通道和非堵塞通道。非堵塞通道不能在文件通道上使用。


    通道类似于一种连接,所以通道是不能循环使用的。通道能够被关闭。关闭能够通过close方法和中断,对通道发送中断信号通道就会关闭。这样的设计初看认为非常别扭,可是这样设计是为了便于在不同的操作系统中实现。


    通道还支持批量写入或读取多个缓冲区。一般的操作系统都从底层支持批量写入或读取缓冲区,因此Java会将批量操作翻译成系统底层的API调用,让操作系统来完毕批量操作,因此速度很快。


    文件通道仅仅能是堵塞通道。比起FileStream,FileChannel还提供了很多其它的操作,比方指定在某个位置写入数据。文件通道的创建须要FileStream或者RandomAccessFile,文件通道的状态和创建时传入的參数状态是保持一致的,文件的位置是同步的。文件通道还提供了force操作,将文件的改动马上写入文件。文件通道提供了truncate操作,用于设置文件的大小。


    文件系统中有个文件洞(File Hole)的概念,就是文件的大小比占用的空间少。比方在文件的1GB位置写入10K数据,那么文件实际善用的空间是10K,而不是1G。


    文件锁一个常见的误区是,每一个文件仅仅能有一个文件锁,不是每一个文件通道对象有一个文件锁,也不是每一个线程有一个文件锁,而是每一个文件有一个文件锁。因此,在同一个JVM中,假设对一个文件创建了两个文件通道,在同一个地方都加上相互排斥锁,是不会堵塞的。也就是说,在JVM内部,文件锁是不起作用的。文件锁要记得释放,最好就是将释放的代码放在finally块中。


    文件映射缓冲区。这样的缓冲区和普通的缓冲区一样,可是数据的内容是放在磁盘上的。映射缓冲区有三种模式,一种是仅仅读,一种是读写,一种是私有。私有模式下,文件的改动是不会写入到文件的,仅仅是保存到缓冲区中。私有模式下,文件的内容会与其它普通的文件通道同步。可是同步的单位是分页,也就是说,私有模式下是否同步跟操作系统的分页大小有关。假设在私有模式下改动文件,那么相应的分页将不再和其它文件通道同步。


    通道之间还能够直接传输,相关的方法是transferTo和transferFrom。有些操作系统内核就支持通道之间的传输,因此性能很高。


    文件映射的load()方法能够将整个文件载入到操作系统的文件缓存中,同一时候文件的内容和磁盘保持同步。


    套接字通道和文件通道不同,支持非堵塞模式。每一个套接字通道相应了一个套接字。这样的通道不能从现有的套接字中创建。
    blockingLock()方法会返回一个Object对象,能够用Java中的synchronizedkeyword对这个对象进行锁定,防止其它的线程对该对象进行改动。套接字通道分为SocketChannel和ServerSocketChannel。ServerSocketChannel仅仅是提供了非堵塞的accept方法。


    数据报通道使用UDP协议进行通信。注意,在接收数据的时候,假设缓冲区的容量不够了,那么多出的数据会被 extbf{丢弃},不会有不论什么现象。发送数据的时候,假设缓冲区太大,超过了系统的发送队列,那么不会有不论什么数据会被发送。数据报通道也有connect方法,该方法仅仅是指定发送对象,并非真正的连接。


    管道通道(PipeChannel)和Unix中的管道通信不是同一个概念。NIO中的管道通道仅仅能在一个JVM内部进行通信,而不是进程间的通信。进程间通信能够通过套接字。管道通信在创建的时候通过Pipe.open()就可以创建一对通道,SinkChannel和SourceChannel。SinkChannel用于写入,SourceChannel用于读取。通过管道能够实现一个线程仅仅顾写入数据,另外一个线程仅仅顾读取数据,有点类似于Python中的generator对象。管道通道最大的用处就是封装。将一个文件通道或者套接字通道封装成管道通道,提高代码的复用程度。经过实验,发现管道内部存在缓冲,就算另外一边没有读取,写入的一边也能够写入大于1K的数据。


    选择器

    选择器的详细实现仅仅能是通过操作系统来完毕,因此性能比較高。


    有关选择器部分的有三个类,Selector、SelectionKey、SelectableChannel。


    Selector用于管理多个可选通道和一堆SelectionKey。select方法会堵塞,返回的不是已经就绪的通道数量,而是在这次调用中成为就绪状态的通道数量。selectedKeys()返回的事实上是一个Set,而Set不支持多线程,所以假设selectedKeys放在另外的线程迭代,那么在迭代的过程中可能会产生ConcurrentModificationException。


    Selector中有三种集合:注冊集合、选择集合、取消集合。选择集合仅仅会添加�不会降低,降低须要通过迭代器手动删除,每处理一次请求就删除相应的SelectionKey。


    选择模式有两种一种是select第二种是epoll。Select是POSIX标准,而Epoll是Linux特有的。Select最多仅仅能监听1024个通道,而Epoll则没有这样的限制。Select每次调用时会扫描全部的通道,因此通道越多性能越差,而Epoll中有一个可用队列,这个队列由操作系统内核来维护的,当一个通道可用时,操作系统就会往队列中添加�通道,因此性能不会随着通道数量的添加�而变差。


    Epoll有两种工作模式,一种是Level Trigger水平触发,还有一种是Edge Trigger边缘触发。默认是水平触发,这样的模式当通道的数据还没有读取完时,下一次选择之后selectionKeys会立即返回没有读完的通道,而边缘触发则不会,边缘触发的性能更高可是程序出错的可能性更大。


    SelectionKey就是通道和选择器的相应关系。提供了readyOps()方法,这种方法返回通道已经就绪的操作。也能够通过isWritable()、isReadable()等方法推断通道是否支持某个操作,这两种方法是等价的。选择键还能够带上一个附件,便于通道获取參数。须要注意的是,附件假设不再使用,应该立即清除掉,否则会造成内存泄露。


    SelectableChannel就是可选通道,它能够在多个Selector中注冊,注冊的时候要提供须要监听的事件,比方OP\_READ、OP\_WRITE。validOps()方法返回这个通道能够监听的操作。JDK中定义了4种兴趣:读、写、连接、接受。SocketChannel是不能接受连接的,所以validOps不会返回接受动作。注冊通道能够反复注冊,可是第二次注冊时仅仅会改动兴趣集,并返回同一个SelectionKey。假设第二次注冊的时候已经调用了cancel()方法,然而Selector还没有来得及更新,就会发生CancelledKeyException。


    关闭通道应该是一个很高速的操作,没有不论什么堵塞。这是JavaNIO的设计目标。这种设计称为异步关闭。


    一般编写代码的时候模板例如以下:
    while(true) {
        selector.select();
    
    
        Iterator<SelectionKey> keys = selector.selectedKeys();
        while(keys.hasNext()) {
            SelectionKey key = keys.next();
    
    
            // 处理事件
            ...
    
    
            // 处理完成之后删除,这样就表示 这次事件已经处理过了
            keys.remove();
        }
    }




    对于多核的计算机而言,仅仅有一个线程在工作是很低效的,为了在多核计算机上提升性能,必须引入多核线程和多个选择器。每一个线程一个选择器,每次接受连接的时候随机分配给一个线程。这是一种方法,第二种方法是当中一个线程用于接受连接,其余的线程专门负责处理业务。
  • 相关阅读:
    【刷题】BZOJ 4059 [Cerc2012]Non-boring sequences
    【刷题】BZOJ 3745 [Coci2015]Norma
    【刷题】BZOJ 2734 [HNOI2012]集合选数
    【刷题】BZOJ 2287 【POJ Challenge】消失之物
    【刷题】BZOJ 2151 种树
    【刷题】BZOJ 2134 单选错位
    【刷题】BZOJ 1924 [Sdoi2010]所驼门王的宝藏
    【刷题】BZOJ 1823 [JSOI2010]满汉全席
    【刷题】BZOJ 1124 [POI2008]枪战Maf
    编程之美 ---> 1.2中国象棋将帅问题
  • 原文地址:https://www.cnblogs.com/yxwkf/p/4009508.html
Copyright © 2011-2022 走看看