Java中IO流类的体系中BIO与NIO

简介: Java中IO流类的体系中BIO与NIO

1 BIO同步阻塞IO

1.1 特性:同步阻塞IO

1.2 特点:一个请求对应一个线程,上下文切换占用的资源很重。

1.3 缺点:无用的请求也会占用一个线程,没有数据达到,也会阻塞。

1.4 改进:通过线程池机制。 但是还是未能解决一个请求一个线程的本质问题,只是稍加改善。

1.5 试用场景:链接数目较少,固定请求。程序比较清晰,一个请求一个线程,容易理解。要求机器配置较高。

2 NIO同步非阻塞IO

2.1 特性:同步非阻塞IO


2.2 特点:利用IO多路复用技术+NIO,多个socket通道对应一个线程


2.3 复用: 多路复用技术:select,poll和epoll ,linux系统下利用epoll多路复用技术性能更高。


2.4 虚拟内存映射文件操作,不需要read或write操作,虚拟内存相当于缓冲区,提升性能。


2.5 修改文件自动flush到文件


2.6 快速处理大文件


 IO多路复用:I/O是指网络I/O,多路指多个TCP连接(即socket或者channel),复用指复用一个或几个线程。意思说一个或一组线程处理多个TCP连接。最大优势是减少系统开销小,不必创建过多的进程/线程,也不必维护这些进程/线程。   


IO多路复用使用两个系统调用(select/poll/epoll和recvfrom),blocking IO只调用了recvfrom;select/poll/epoll 核心是可以同时处理多个connection,而不是更快,所以连接数不高的话,性能不一定比多线程+阻塞IO好,多路复用模型中,每一个socket,设置为non-blocking,阻塞是被select这个函数block,而不是被socket阻塞的。

3 select机制

客户端操作服务器时就会产生这三种文件描述符(简称fd):writefds(写)、readfds(读)、和exceptfds(异常)。select会阻塞住监视3类文件描述符,等有数据、可读、可写、出异常 或超时、就会返回;返回后通过遍历fdset整个数组来找到就绪的描述符fd,然后进行对应的IO操作。


优点:几乎在所有的平台上支持,跨平台支持性好


缺点:由于是采用轮询方式全盘扫描,会随着文件描述符FD数量增多而性能下降。   每次调用 select(),需要把 fd 集合从用户态拷贝到内核态,并进行遍历(消息传递都是从内核到用户空间)   默认单个进程打开的FD有限制是1024个,可修改宏定义,但是效率仍然慢。


3.1 poll机制 

基本原理与select一致,也是轮询+遍历;唯一的区别就是poll没有最大文件描述符限制(使用链表的方式存储fd)。


3.2 epoll机制 

没有fd个数限制,用户态拷贝到内核态只需要一次,使用时间通知机制来触发。通过epoll_ctl注册fd,一旦fd就绪就会通过callback回调机制来激活对应fd,进行相关的io操作。 epoll之所以高性能是得益于它的三个函数   


1)epoll_create()系统启动时,在Linux内核里面申请一个B+树结构文件系统,返回epoll对象,也是一个fd   


2)epoll_ctl() 每新建一个连接,都通过该函数操作epoll对象,在这个对象里面修改添加删除对应的链接fd, 绑定一个callback函数   


3)epoll_wait() 轮训所有的callback集合,并完成对应的IO操作


优点:   


没fd这个限制,所支持的FD上限是操作系统的最大文件句柄数,1G内存大概支持10万个句柄效率提高,使用回调通知而不是轮询的方式,不会随着FD数目的增加效率下降   


内核和用户空间mmap同一块内存实现(mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间)


例子:


100万个连接,里面有1万个连接是活跃,我们可以对比 select、poll、epoll 的性能表现   


select:不修改宏定义默认是1024,l则需要100w/1024=977个进程才可以支持 100万连接,会使得CPU性能特别的差。   


poll: 没有最大文件描述符限制,100万个链接则需要100w个fd,遍历都响应不过来了,还有空间的拷贝消耗大量的资源。   


epoll: 请求进来时就创建fd并绑定一个callback,主需要遍历1w个活跃连接的callback即可,即高效又不用内存拷贝。

52eb89877d204ff2aede8ed5a104c9a7.png

简单来说:select和poll会一直循环遍历所有的连接事件,cpu会空转消耗资源。而epoll解决了这些问题,只有时间发送才会进行操作,select最多支持1024个连接而jdk1.4版本poll无上限,jdk1.5解决了所有问题。

4 Java中的IO模型

在JDK1.4之前,基于Java所有的socket通信都采⽤了同步阻塞模型(BIO),这种模型性能低下,当时⼤型的服务均采⽤C或C++开发,因为它们可以直接使⽤操作系统提供的异步IO或者AIO,使得性能得到⼤幅提升。

2002年,JDK1.4发布,新增了java.nio包,提供了许多异步IO开发的API和类库。新增的NIO,极⼤的促进了基于Java的异步⾮阻塞的发展和应⽤。

2011年,JDK7发布,将原有的NIO进⾏了升级,称为NIO2.0,其中也对AIO进⾏了⽀持


4.1 BIO模型


java中的BIO是blocking I/O的简称,它是同步阻塞型IO,其相关的类和接⼝在java.io下。

BIO模型简单来讲,就是服务端为每⼀个请求都分配⼀个线程进⾏处理,如下:

ce7c9572df63446f804aa5cfbac6a663.png

示例代码:

public class BIOServer {
    public static void main(String[] args) throws Exception {
        ServerSocket serverSocket = new ServerSocket(6666);
        ExecutorService executorService = Executors.newCachedThreadPool();
        while (true) {
            System.out.println("等待客户端连接。。。。");
            Socket socket = serverSocket.accept(); //阻塞
            executorService.execute(() -> {
                try {
                    InputStream inputStream = socket.getInputStream(); //阻塞
                    byte[] bytes = new byte[1024];
                    while (true){
                        int length = inputStream.read(bytes);
                        if(length == -1){
                            break;
                        }
                        System.out.println(new String(bytes, 0, length, "UTF-
                                8"));
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

这种模式存在的问题:

客户端的并发数与后端的线程数成1:1的⽐例,线程的创建、销毁是⾮常消耗系统资源的,随着并

发量增⼤,服务端性能将显著下降,甚⾄会发⽣线程堆栈溢出等错误。

当连接创建后,如果该线程没有操作时,会进⾏阻塞操作,这样极⼤的浪费了服务器资源。

4.2 NIO模型

NIO,称之为New IO 或是 non-block IO (⾮阻塞IO),这两种说法都可以,其实称之为⾮阻塞IO更恰当⼀些。


NIO相关的代码都放在了java.nio包下,其三⼤核⼼组件:Buffer(缓冲区)、Channel(通道)、Selector(选择器/多路复⽤器)


Buffer

在NIO中,所有的读写操作都是基于缓冲区完成的,底层是通过数组实现的,常⽤的缓冲区是

ByteBuffer,每⼀种java基本类型都有对应的缓冲区对象(除了Boolean类型),如:

CharBuffer、IntBuffer、LongBuffer等。


Channel

在BIO中是基于Stream实现,⽽在NIO中是基于通道实现,与流不同的是,通道是双向的,

既可以读也可以写。

Selector

可以看出,NIO模型要优于BIO模型,主要是:

通过多路复⽤器就可以实现⼀个线程处理多个通道,避免了多线程之间的上下⽂切换导致系统开销

过⼤。

NIO⽆需为每⼀个连接开⼀个线程处理,并且只有通道真正有有事件时,才进⾏读写操作,这样⼤

⼤的减少了系统开销。

示例代码:

public class SelectorDemo {
    /**
     * 注册事件
     *
     * @return
     */
    private Selector getSelector() throws Exception {
//获取selector对象
        Selector selector = Selector.open();
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false); //⾮阻塞
//获取通道并且绑定端⼝
        ServerSocket socket = serverSocketChannel.socket();
        socket.bind(new InetSocketAddress(6677));
//注册感兴趣的事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        return selector;
    }
    public void listen() throws Exception {
        Selector selector = this.getSelector();
        while (true) {
            selector.select(); //该⽅法会阻塞,直到⾄少有⼀个事件的发⽣
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                process(selectionKey, selector);
                iterator.remove();
            }
        }
    }
    private void process(SelectionKey key, Selector selector) throws Exception
    {
        if(key.isAcceptable()){ //新连接请求
            ServerSocketChannel server = (ServerSocketChannel)key.channel();
            SocketChannel channel = server.accept();
            channel.configureBlocking(false); //⾮阻塞
            channel.register(selector, SelectionKey.OP_READ);
        }else if(key.isReadable()){ //读数据
            SocketChannel channel = (SocketChannel)key.channel();
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            channel.read(byteBuffer);
            System.out.println("form 客户端 " + new String(byteBuffer.array(),
                    0, byteBuffer.position()));
        }
    }
    public static void main(String[] args) throws Exception {
        new SelectorDemo().listen();
    }
}

4.3 AIO模型

在NIO中,Selector多路复⽤器在做轮询时,如果没有事件发⽣,也会进⾏阻塞,如何能把这个阻塞也优化掉呢?那么AIO就在这样的背景下诞⽣了。

AIO是asynchronous I/O的简称,是异步IO,该异步IO是需要依赖于操作系统底层的异步IO实现。

AIO的基本流程是:⽤户线程通过系统调⽤,告知kernel内核启动某个IO操作,⽤户线程返回。kernel内核在整个IO操作(包括数据准备、数据复制)完成后,通知⽤户程序,⽤户执⾏后续的业务操作。


kernel的数据准备

将数据从⽹络物理设备(⽹卡)读取到内核缓冲区。

kernel的数据复制

将数据从内核缓冲区拷⻉到⽤户程序空间的缓冲区。

⽬前AIO模型存在的不⾜:

需要完成事件的注册与传递,这⾥边需要底层操作系统提供⼤量的⽀持,去做⼤量的⼯作。


Windows 系统下通过 IOCP 实现了真正的异步 I/O。但是,就⽬前的业界形式来说,Windows 系

统,很少作为百万级以上或者说⾼并发应⽤的服务器操作系统来使⽤。


⽽在 Linux 系统下,异步IO模型在2.6版本才引⼊,⽬前并不完善。所以,这也是在 Linux 下,实

现⾼并发⽹络编程时都是以 NIO 多路复⽤模型模式为主。


目录
相关文章
|
2月前
|
Java 开发者
重学Java基础篇—Java类加载顺序深度解析
本文全面解析Java类的生命周期与加载顺序,涵盖从加载到卸载的七个阶段,并深入探讨初始化阶段的执行规则。通过单类、继承体系的实例分析,明确静态与实例初始化的顺序。同时,列举六种触发初始化的场景及特殊场景处理(如接口初始化)。提供类加载完整流程图与记忆口诀,助于理解复杂初始化逻辑。此外,针对空指针异常等问题提出排查方案,并给出最佳实践建议,帮助开发者优化程序设计、定位BUG及理解框架机制。最后扩展讲解类加载器层次与双亲委派机制,为深入研究奠定基础。
88 0
|
28天前
|
Java 数据安全/隐私保护
Java 类和对象
本文介绍了Java编程中类和对象的基础知识,作为面向对象编程(OOP)的核心概念。类是对象的蓝图,定义实体类型;对象是具体实例,包含状态和行为。通过示例展示了如何创建表示汽车的类及其实例,并说明了构造函数、字段和方法的作用。同时,文章还探讨了访问修饰符的使用,强调封装的重要性,如通过getter和setter控制字段访问。最后总结了类与对象的关系及其在Java中的应用,并建议进一步学习继承等概念。
|
2月前
|
缓存 安全 Java
《从头开始学java,一天一个知识点》之:输入与输出:Scanner与System类
你是否也经历过这些崩溃瞬间?三天教程连`i++`和`++i`都说不清,面试时`a==b`与`equals()`区别大脑空白,代码总是莫名报NPE。这个系列就是为你打造的Java「速效救心丸」!每天1分钟,地铁通勤、午休间隙即可学习。直击高频考点和实际开发中的“坑位”,拒绝冗长概念,每篇都有可运行代码示例。涵盖输入输出基础、猜数字游戏、企业编码规范、性能优化技巧、隐藏技能等。助你快速掌握Java核心知识,提升编程能力。点赞、收藏、转发,助力更多小伙伴一起成长!
53 19
|
2月前
|
存储 监控 安全
重学Java基础篇—类的生命周期深度解析
本文全面解析了Java类的生命周期,涵盖加载、验证、准备、解析、初始化、使用及卸载七个关键阶段。通过分阶段执行机制详解(如加载阶段的触发条件与技术实现),结合方法调用机制、内存回收保护等使用阶段特性,以及卸载条件和特殊场景处理,帮助开发者深入理解JVM运作原理。同时,文章探讨了性能优化建议、典型异常处理及新一代JVM特性(如元空间与模块化系统)。总结中强调安全优先、延迟加载与动态扩展的设计思想,并提供开发建议与进阶方向,助力解决性能调优、内存泄漏排查及框架设计等问题。
69 5
|
2月前
|
缓存 安全 Java
《从头开始学java,一天一个知识点》之:字符串处理:String类的核心API
🌱 **《字符串处理:String类的核心API》一分钟速通!** 本文快速介绍Java中String类的3个高频API:`substring`、`indexOf`和`split`,并通过代码示例展示其用法。重点提示:`substring`的结束索引不包含该位置,`split`支持正则表达式。进一步探讨了String不可变性的高效设计原理及企业级编码规范,如避免使用`new String()`、拼接时使用`StringBuilder`等。最后通过互动解密游戏帮助读者巩固知识。 (上一篇:《多维数组与常见操作》 | 下一篇预告:《输入与输出:Scanner与System类》)
79 11
|
1月前
|
Java
java中一个接口A,以及一个实现它的类B,一个A类型的引用对象作为一个方法的参数,这个参数的类型可以是B的类型吗?
本文探讨了面向对象编程中接口与实现类的关系,以及里氏替换原则(LSP)的应用。通过示例代码展示了如何利用多态性将实现类的对象传递给接口类型的参数,满足LSP的要求。LSP确保子类能无缝替换父类或接口,不改变程序行为。接口定义了行为规范,实现类遵循此规范,从而保证了多态性和代码的可维护性。总结来说,接口与实现类的关系天然符合LSP,体现了多态性的核心思想。
37 0
|
2月前
|
安全 IDE Java
重学Java基础篇—Java Object类常用方法深度解析
Java中,Object类作为所有类的超类,提供了多个核心方法以支持对象的基本行为。其中,`toString()`用于对象的字符串表示,重写时应包含关键信息;`equals()`与`hashCode()`需成对重写,确保对象等价判断的一致性;`getClass()`用于运行时类型识别;`clone()`实现对象复制,需区分浅拷贝与深拷贝;`wait()/notify()`支持线程协作。此外,`finalize()`已过时,建议使用更安全的资源管理方式。合理运用这些方法,并遵循最佳实践,可提升代码质量与健壮性。
81 1
|
2月前
|
存储 Java 编译器
课时11:综合实战:简单Java类
本次分享的主题是综合实战:简单 Java 类。主要分为两个部分: 1.简单 Java 类的含义 2.简单 Java 类的开发
|
2月前
|
Oracle Java 关系型数据库
课时37:综合实战:数据表与简单Java类映射转换
今天我分享的是数据表与简单 Java 类映射转换,主要分为以下四部分。 1. 映射关系基础 2. 映射步骤方法 3. 项目对象配置 4. 数据获取与调试
|
2月前
|
Java
java常见的集合类有哪些
Map接口和Collection接口是所有集合框架的父接口: 1. Collection接口的子接口包括:Set接口和List接口 2. Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及 Properties等 3. Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等 4. List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等