写入和读取都可能会被阻塞,比如Socket的read方法等消息接收完后也被阻塞(失去CPU的控制权,类似于多线程锁竞争失败被阻塞),一直等待新消息,可是访问量较大和性能要求较高时,当然可以用多个线程来维护收和发,不过在现在需要大量长连接的情况下,不可能保持这么多连接,而且线程开启的数量必然也是有限的。 这时候便需要使用NIO了。(NIO是New IO的意思,它可以设置为非阻塞IO,也可以为阻塞IO)
主要有仨部分, 1.缓冲区ByteBuffer等(除了boolean其他基本类型都有) 2.通道Channel等。(FileChannel, SocketChannel,ServerSocketChannel,DatagramChannel等) 3.选择器Selector(非阻塞式IO的情况下,用于轮询是否有数据进来,用户线程便不会被阻塞,只需要服务器这边监听就好了) 无论阻塞还是非阻塞,都需要创建通道与缓冲区,通道相当于修了条路,缓冲区相当于车,光有路没有车拉货(数据)也不行,两者都得有。 选择器就相当于快递站点,会在到货时做出相应通知,而不用一直在那儿傻等。 客户端的代码很简单,与一般的基本上无异,服务端稍微有点复杂
public void server() throws IOException { SocketChannel socketChannel = null; try(ServerSocketChannel ssChannel = ServerSocketChannel.open(); Selector selector = Selector.open()) { //设置为非阻塞方式 ssChannel.configureBlocking(false); ssChannel.bind(new InetSocketAddress(10086)); //注册监听事件 ssChannel.register(selector, SelectionKey.OP_ACCEPT); while (selector.select() > 0) { //获取选择器的迭代器 Iterator<SelectionKey> it = selector.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey sk = it.next(); //是否为目标键 if (sk.isAcceptable()) { socketChannel = ssChannel.accept(); socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ); } else if (sk.isReadable()) { socketChannel = (SocketChannel) sk.channel(); ByteBuffer buf = ByteBuffer.allocate(1024); while (socketChannel.read(buf) != -1) { buf.flip(); System.out.println(new String(buf.array(), 0, buf.limit())); buf.clear(); } } } it.remove(); } }finally { if(socketChannel!=null){ socketChannel.close(); } } }这个程序将监听与处理放在了一个线程中,一般应用时并不这样处理,而是一个线程以阻塞式监听客户端的请求,,另一个线程专门负责处理请求,这个线程便会采用NIO非阻塞的方式。像Web服务器Tomcat和Jetty都是如此处理的。
可简单的看成一个基本类型的数组。 它有四个参数: 1、capacity:容量,不可变 2、limit:限制,表示缓冲区中可以操作数据的大小 3、position:位置 4.mark记录当前position的位置(默认为0),可通过reset()恢复到mark的位置
我觉得最有意思的地方是,它读写共用一套指针,所以写后读需要转换模式(更改position指针的位置,flip()) 然后数据读完了,可使用claar"清空"之前的数据,或者读取接着写(相当于append),这个清空实际上只是将指针初始化,然后再写入时就会覆盖。 Buffer有两种创建方式, 1.用户内存中,就是在JVM的堆内存中,这种好处是不会堆外溢出,坏处是前文所述,操作磁盘,需要由操作系统完成,所以需要在用户态和核心态之间切换,这就会比较耗费资源与时间。(适用于并发量少于1000,I/O操作较少) 2.直接操作操作系统的缓冲区,通过内存页映射,直接访问I/O。不过每次调用会调用一次System.gc(),还可能造成内存泄露的问题。(适用于数据量大,生命周期较长)
1.Channel.tansferFrom.tansferTo 这个方法是是内存页内直接转移的,效率会比传统方式高。
public void testTransfor() throws IOException { long start = System.currentTimeMillis(); try(FileChannel inChannel = FileChannel.open(Paths.get("src/main/resources/copy.png"),StandardOpenOption.READ); FileChannel outChannel = FileChannel.open(Paths.get("src/main/resources/copy11.png"),StandardOpenOption.WRITE,StandardOpenOption.CREATE)){ inChannel.transferTo(0,inChannel.size(),outChannel); } System.out.println(System.currentTimeMillis()-start); }2.FileChannel.map 将文件按照一定大小块映射为内存区域,当程序访问这个内存区域时将直接操作这个文件数据,省去了内核空间向用户空间复制的损耗。适合对大文件的只读性操作,如大文件的MD5校验。
@Test public void testCopyD() throws IOException { long start = System.currentTimeMillis(); try(FileChannel inChannel = FileChannel.open(Paths.get("src/main/resources/copy.png"), StandardOpenOption.READ); FileChannel outChannel = FileChannel.open(Paths.get("src/main/resources/newCopy.png"),StandardOpenOption.WRITE,StandardOpenOption.READ)) { MappedByteBuffer inMap = inChannel.map(FileChannel.MapMode.READ_ONLY,0,inChannel.size()); //注意由于这里只有读写模式,所以上面管道的设置中也需要添加上读 MappedByteBuffer outMap = outChannel.map(FileChannel.MapMode.READ_WRITE,0,outChannel.size()); byte[] dst = new byte[inMap.limit()]; inMap.get(dst); outMap.put(dst); } System.out.println(System.currentTimeMillis()-start); }1.性能检测 Linux下可使用iostat命令查看io的情况 iowait参数不应该超过25% 还有一个参数就是IOPS,即每秒读写次数,应用程序所需的最低IOPS。 通常采用RAID(磁盘阵列)技术来优化,即不同的磁盘组合起来以提高I/O性能 计算公式为: (磁盘数×每块磁盘的IOPS)/(磁盘读的吞吐量+RAID因子×磁盘写的吞吐量) = IOPS 2.提升I/O性能 通常的方法有:
增加缓存,减少磁盘访问次数优化磁盘的管理系统,设计最优的磁盘方式策略,以及侧畔的寻址策略,底层操作操作系统层面考虑设计合理的磁盘存储数据块,例如设计索引。应用合理的RAID策略提升磁盘IO 磁盘阵列说明RAID 0数据被平均写到多个数据阵列中,写读并行,IOPS提升一倍RAID 1提升数据的安全性,将数据分别复制到多个磁盘阵列中,并不能提升IOPS。RAID 5将数据平均写到所有磁盘阵列总数减1的磁盘中,往另外一个磁盘中写入这份数据的奇偶校验信息。如果其中一个磁盘损坏,可以通过其他磁盘的数据和这个数据的奇偶校验信息来恢复RAID 0+1如名字一样,备份+分组读写以上设置均为临时性的,重启便会丢失。 还可用cat /proc/net/netstat 查看TCP的统计信息 cat /proc/net/snmp查看当前系统的连接情况。 netstat -s查看网络的统计信息。
优化存在以下原则 1.减少传输次数,具体可以设置缓存和合并传输。 2.减少网络传输的数据量的大小,通常是将数据压缩后再传输,还有尽可能使用简单的协议。 3.尽量减少编码,尽量提前将字符转换为字节的形式,将编码过程提前。
异步与同步的选择本质是可靠性与性能的平衡,不存在完美的选择
阻塞与非阻塞,阻塞会让被阻塞的线程的cpu停下去等待较慢操作完成,而非阻塞就不存在cpu利用率下降的问题,可是它会造成线程的频繁切换,视情况选择阻塞还是非阻塞
以上同步与否 和 阻塞与否 结合起来,就又可以分为四种情况
同步阻塞:I/O性能一般很差,CPU大部分时间处于空闲状态。最常用的一种用法,使用简单同步非阻塞:对于网络I/O长连接且传输数据不是很多时,提升性能及其有效。这种方法能够提升I/O效率,但是不能补偿CPU消耗。异步阻塞:分布式数据库中常用,在分布式数据库中写一条记录,通常会有一份是同步阻塞的记录,然后其他异步阻塞的备份记录写到其他机器上。异步非阻塞:使用非常的复杂,只有复杂的分布式才会使用,集群之间的消息同步机制一般采用此方式,适用于同时要传多份相同的数据到集群中不同的机器,同时数据的传输量虽然不大却非常频繁的情况。这种网络I/O性能可达最高。将一个类的接口转换为客户端所能接受的另一种接口,从而使两个接口不匹配的两个类能够一起工作。 在IO中就使用到了这种设计模式(字符流转换为字节流或者字节流转换为字符流)
以上的类图是字节流转换为字符流,由于需要解码,所以InputStreamReader以组合的形式纳入了一个StringDecoder对象,这里的源角色就是InputStream,目标就是Reader,自然适配器就是InputStreamReader啦。
将某个类再以类的层面再封装一下,使得类能够更强大。 而且使用上不应该与原类有啥不同。 详细可看本文 InputStream是抽象实体,定义了规则,FileInputStream是具体的实体,实现了所有的接口。 而FilterInputStream则是抽象装饰器,定义规则,而BufferInputStream则是具体的装饰器,实现规则,在不改变原来的情况下进行增强。