Java IO(非常详细)

2年前 (2022) 程序员胖胖胖虎阿
174 0 0

注:本篇文章部分图片可能来源网络,侵删。

关于IO会涉及到阻塞、非阻塞、多路复用、同步、异步、BIO、NIO、AIO等几个知识点。知识点虽然不难但平常经常容易搞混,特此Mark下,与君共勉。

文章目录

    • 1、同步IO
      • 1.1 阻塞IO
      • 1.2 非阻塞IO
      • 1.3 IO多路复用(!)
        • 1.3.1 select
        • 1.3.2 poll
        • 1.3.3 epoll
    • 2、异步IO
    • 3、Java IO
      • 3.1 BIO
      • 3.2 NIO
      • 3.3 AIO

1、同步IO

1.1 阻塞IO

Java IO(非常详细)
如图所示,阻塞式IO情况下,当系统调用read后,如果此时内存数据未准备好,用户线程会被阻塞,等数据准备好,并从内核缓存区将数据拷贝至用户空间后,read指令才会返回。

1.2 非阻塞IO

Java IO(非常详细)
非阻塞IO发出read请求后发现数据没准备好,会继续往下执行,此时应用程序会不断轮询polling内核询问数据是否准备好,当数据没有准备好时,内核立即返回EWOULDBLOCK错误。直到数据被拷贝到应用程序缓冲区,read请求才获取到结果。这种方式虽然较阻塞式有了性能的提升,但依旧会对性能造成影响,因为这过程是需要应用程序不断对内存进行轮询操作。

1.3 IO多路复用(!)

IO多路复用就解决了非阻塞式IO的缺点,即它不会对内存进行轮询操作,当内核数据准备完毕,以事件通知机制告知应用进程已经准备好了,在此之前,应用进程可以忙其他的工作,从而减少对性能的影响。通过多路IO复用,能使得一个进程同时处理多路IO,提升服务器吞吐量。

1.3.1 select

select函数
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout);

select()的机制中提供一种fd_set的数据结构,实际上是一个long类型的数组,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪一Socket或文件可读。

使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。

缺点:

  1. select只能监控1024个连接
  2. select 不是线程安全的,如果你把一个socket加入到select, 然后突然另外一个线程发现这个socket不用,要收回,这个select 不支持的。
  3. 每次调用select,都需要把fd_set集合从用户态拷贝到内核态,如果fd_set集合很大时,那这个开销也很大。

1.3.2 poll

poll的机制与select类似,与select在本质上没有多大差别。poll相较于select只解决了最大连接数的问题,即poll没有最大文件描述符的限制。

1.3.3 epoll

解决了select、poll存在的绝大部分问题。

在select/poll时代,服务器进程每次都把这100万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll一般只能处理几千的并发连接。

epoll的设计和实现与select完全不同。epoll通过在Linux内核中申请一个简易的文件系统(文件系统一般用什么数据结构实现?B+树)。把原先的select/poll调用分成了3个部分:

1)调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)

2)调用epoll_ctl向epoll对象中添加这100万个连接的套接字

3)调用epoll_wait收集发生的事件的连接

如此一来,要实现上面说是的场景,只需要在进程启动时建立一个epoll对象,然后在需要的时候向这个epoll对象中添加或者删除连接。同时,epoll_wait的效率也非常高,因为调用epoll_wait时,并没有一股脑的向操作系统复制这100万个连接的句柄数据,内核也不需要去遍历全部的连接。

Java IO(非常详细)

2、异步IO

同步跟异步的区别在于数据从内核空间拷贝到用户空间是否由用户线程完成.这里又分为同步阻塞同步非阻塞.

  • 同步阻塞:此时一个线程维护一个连接,该线程完成数据到读写跟处理到全部过程,数据读写时时线程是被阻塞的。
  • 同步非阻塞:非阻塞的意思是用户线程发出读请求后,读请求不会阻塞当前用户线程,不过用户线程还是要不断的去主动判断数据是否准备OK了。此时还是会阻塞等待内核复制数据到用户进程。他与同步BIO区别是使用一个连接全程等待。

如,IO多路复用就是一种同步非阻塞——
Java IO(非常详细)
而异步IO,用户进行读或者写后,将立刻返回,由内核去完成数据读取以及拷贝工作,完成后通知用户,并执行回调函数(用户提供的callback),此时数据已从内核拷贝到用户空间,用户线程只需要对数据进行处理即可,不需要关注读写,用户不需要等待内核对数据的复制操作,用户在得到通知时数据已经被复制到用户空间。
Java IO(非常详细)

3、Java IO

在Java中,我们使用socket进行网络通信,IO主要有三种模式

  1. BIO:同步阻塞IO
  2. NIO:同步非阻塞IO
  3. AIO:异步非阻塞IO

3.1 BIO

Blocking IO。每个客户端的Socket连接请求,服务端都会对应有个处理线程与之对应,对于没有分配到处理线程的连接就会被阻塞或者拒绝。相当于是一个连接一个线程。
Java IO(非常详细)
特点:

  1. 使用一个独立的线程维护一个socket连接,随着连接数量的增多,对虚拟机造成一定压力。
  2. 使用流来读取数据,流是阻塞的,当没有可读/可写数据时,线程等待,会造成资源的浪费。

3.2 NIO

No blocking IO,同步非阻塞IO:服务器端保存一个Socket连接列表,然后对这个列表进行轮询。

如果发现某个Socket端口上有数据可读时说明读就绪,则调用该socket连接的相应读操作。

如果发现某个 Socket端口上有数据可写时说明写就绪,则调用该socket连接的相应写操作。

如果某个端口的Socket连接已经中断,则调用相应的析构方法关闭该端口。这样能充分利用服务器资源,效率得到了很大提高。

在进行IO操作请求时候再用个线程去处理,是一个请求一个线程。Java中使用SelectorChannelBuffer来实现上述效果。
Java IO(非常详细)
每个线程中包含一个Selector对象,它相当于一个通道管理器,可以实现在一个线程中处理多个通道的目的,减少线程的创建数量。

远程连接对应一个channel,数据的读写通过buffer均在同一个channel中完成,并且数据的读写是非阻塞的。

通道创建后需要注册在selector中,同时需要为该通道注册感兴趣事件(客户端连接服务端事件、服务端接收客户端连接事件、读事件、写事件),selector线程需要采用轮训的方式调用selector的select函数,直到所有注册通道中有兴趣的事件发生,则返回,否则一直阻塞。

这里介绍一下三个相关概念——

  1. selector:Selector 允许单线程处理多个Channel。如果你的应用打开了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便。要使用Selector,得向Selector注册Channel,然后调用他的select方法,这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子入有新连接接进来,数据接收等。
  2. Channel:基本上所有的IO在NIO中都从一个Channel开始。Channel有点像流,数据可以从channel读到buffer,也可以从buffer写到channel。
  3. Buffer:缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松的使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变换情况,Channel提供从文件,网络读取数据的渠道,但是读取或者写入的数据都必须经由Buffer。

3.3 AIO

AIO是异步非阻塞IO,相比NIO更进一步,进程读取数据时只负责发送跟接收指令,数据的准备工作完全由操作系统来处理。

版权声明:程序员胖胖胖虎阿 发表于 2022年10月30日 下午2:00。
转载请注明:Java IO(非常详细) | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...