《深入分析JavaWeb技术内幕》第二章深入Java I/O的工作机制 (下)

NIO的工作方式

上一章学的全是BIO(阻塞式I/O)

写入和读取都可能会被阻塞,比如Socket的read方法等消息接收完后也被阻塞(失去CPU的控制权,类似于多线程锁竞争失败被阻塞),一直等待新消息,可是访问量较大和性能要求较高时,当然可以用多个线程来维护收和发,不过在现在需要大量长连接的情况下,不可能保持这么多连接,而且线程开启的数量必然也是有限的。
这时候便需要使用NIO了。(NIO是New IO的意思,它可以设置为非阻塞IO,也可以为阻塞IO)

NIO工作机制

在这里插入图片描述主要有仨部分,
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都是如此处理的。

Buffer的工作方式

可简单的看成一个基本类型的数组。
它有四个参数:
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);
    }

I/O调优

1.性能检测
Linux下可使用iostat命令查看io的情况
在这里插入图片描述
iowait参数不应该超过25%
还有一个参数就是IOPS,即每秒读写次数,应用程序所需的最低IOPS。
通常采用RAID(磁盘阵列)技术来优化,即不同的磁盘组合起来以提高I/O性能
计算公式为:
(磁盘数×每块磁盘的IOPS)/(磁盘读的吞吐量+RAID因子×磁盘写的吞吐量) = IOPS
2.提升I/O性能
通常的方法有:

  1. 增加缓存,减少磁盘访问次数
  2. 优化磁盘的管理系统,设计最优的磁盘方式策略,以及侧畔的寻址策略,底层操作操作系统层面考虑
  3. 设计合理的磁盘存储数据块,例如设计索引。
  4. 应用合理的RAID策略提升磁盘IO
磁盘阵列说明
RAID 0数据被平均写到多个数据阵列中,写读并行,IOPS提升一倍
RAID 1提升数据的安全性,将数据分别复制到多个磁盘阵列中,并不能提升IOPS。
RAID 5将数据平均写到所有磁盘阵列总数减1的磁盘中,往另外一个磁盘中写入这份数据的奇偶校验信息。如果其中一个磁盘损坏,可以通过其他磁盘的数据和这个数据的奇偶校验信息来恢复
RAID 0+1如名字一样,备份+分组读写

TCP网络参数调优

网络参数说明
echo “1024 65535”> sudo /proc/sys/net/ipv4/ip_local_port_range设置向外链接可用端口范围
echo 1>/proc/sys/net/ipv4/tcp_tw_reuse设置time_wait连接重用
echo 1>/proc/sys/net/ipv4/tcp_tw_recycle设置快速回收time_wait连接
echo 180000>/proc/sys/net/ipv4/tcp_max_tw_buckets设置最大time_wait连接长度
echo 0>/proc/sys/net/ipv4/tcp_timestamps表示是否启用以一种比超时重发更精确的方法来启用对RTT的计算
echo 1>/proc/sys/net/ipv4/tcp_windows_scaling设置TCP/IP滑动窗口是否可变
echo 20000>/proc/sys/net/ipv4/tcp_max_syn_backlog设置最大等待处于客户端还没有应答回来的连接数
echo 10000>/proc/sys/net/core/somexconn设置每一个处于监听状态的端口的监听队列的长度
echo 10000>/proc/sys/net/core/netdev_max_backlog设置最大等待CPU处理的包的数目
echo 10000>/proc/sys/fs/file-max设置最大打开文件数
echo 15>/proc/sys/net/ipv4/tcp_fin_timeout/设置FIN-WAIT-2状态回收时间
echo 16777216>/proc/sys/net/core/rmem_max设置套接字数据接收缓冲大小
echo 262144>/proc/sys/net/core/rmem_default设置默认的系统套接字数据接收缓冲大小
echo 16777216>/proc/sys/net/core/wmem_max套接字发送最大缓冲
echo 262144>/proc/sys/net/core/wmem_default设置默认的系统套接字数据发送缓冲大小
echo “4096 87380 16777216”>/proc/sys/net/ipv4/tcp_rmem设置最大的TCP发送缓存大小,三个值分别对应,最小,默认,最大
echo “4096 87380 16777216”>/proc/sys/net/ipv4/tcp_wmem设置最大的TCP接收缓存大小,三个值分别对应,最小,默认,最大

以上设置均为临时性的,重启便会丢失。
还可用cat /proc/net/netstat 查看TCP的统计信息
cat /proc/net/snmp查看当前系统的连接情况。
netstat -s查看网络的统计信息。

查看网络IO优化

优化存在以下原则
1.减少传输次数,具体可以设置缓存和合并传输。
2.减少网络传输的数据量的大小,通常是将数据压缩后再传输,还有尽可能使用简单的协议。
3.尽量减少编码,尽量提前将字符转换为字节的形式,将编码过程提前。

异步与同步的选择本质是可靠性与性能的平衡,不存在完美的选择

阻塞与非阻塞,阻塞会让被阻塞的线程的cpu停下去等待较慢操作完成,而非阻塞就不存在cpu利用率下降的问题,可是它会造成线程的频繁切换,视情况选择阻塞还是非阻塞

以上同步与否 和 阻塞与否 结合起来,就又可以分为四种情况

  1. 同步阻塞:I/O性能一般很差,CPU大部分时间处于空闲状态。最常用的一种用法,使用简单
  2. 同步非阻塞:对于网络I/O长连接且传输数据不是很多时,提升性能及其有效。这种方法能够提升I/O效率,但是不能补偿CPU消耗。
  3. 异步阻塞:分布式数据库中常用,在分布式数据库中写一条记录,通常会有一份是同步阻塞的记录,然后其他异步阻塞的备份记录写到其他机器上。
  4. 异步非阻塞:使用非常的复杂,只有复杂的分布式才会使用,集群之间的消息同步机制一般采用此方式,适用于同时要传多份相同的数据到集群中不同的机器,同时数据的传输量虽然不大却非常频繁的情况。这种网络I/O性能可达最高。

适配器模式

将一个类的接口转换为客户端所能接受的另一种接口,从而使两个接口不匹配的两个类能够一起工作。
在IO中就使用到了这种设计模式(字符流转换为字节流或者字节流转换为字符流)

在这里插入图片描述以上的类图是字节流转换为字符流,由于需要解码,所以InputStreamReader以组合的形式纳入了一个StringDecoder对象,这里的源角色就是InputStream,目标就是Reader,自然适配器就是InputStreamReader啦。

装饰模式

将某个类再以类的层面再封装一下,使得类能够更强大。
而且使用上不应该与原类有啥不同。
详细可看本文
在这里插入图片描述InputStream是抽象实体,定义了规则,FileInputStream是具体的实体,实现了所有的接口。
而FilterInputStream则是抽象装饰器,定义规则,而BufferInputStream则是具体的装饰器,实现规则,在不改变原来的情况下进行增强。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值