美文网首页
【Java基础】- IO 学习

【Java基础】- IO 学习

作者: lconcise | 来源:发表于2019-12-20 22:33 被阅读0次

    JAVA IO

    Java IO 即 Java 输入输出。在开发应用软件时,很多时候都需要和各种输入输出相关的媒介打交道。与媒介进行 IO 操作的过程十分复杂,需要考虑众多因素,比如:进行 IO 操作媒介的类型(文件、控制台、网络)、通信方式(顺序、随机、二进制、按字符、按字、按行等等)。

    Java 类库提供了相应的类来解决这些难题,这些类就位于 java.io 包中, 在整个 java.io 包中最重要的就是 5 个类和一个接口。5 个类指的是 File、OutputStream、InputStream、Writer、Reader;一个接口指的是 Serializable。

    由于老的 Java IO 标准类提供 IO 操作(如 read(),write())都是同步阻塞的,因此,IO 通常也被称为阻塞 IO(即 BIO,Blocking I/O)。

    JAVA-NIO

    在 JDK1.4 之后,为了提高 Java IO 的效率,Java 又提供了一套 New IO(NIO),原因在于它相对于之前的 IO 类库是新增的。此外,旧的 IO 类库提供的 IO 方法是阻塞的,New IO 类库则让 Java 可支持非阻塞 IO,所以,更多的人喜欢称之为非阻塞 IO(Non-blocking IO)。

    *注意:异步只有异步,同步才有阻塞和非阻塞的说法!

    IO模型

    1. 阻塞式IO(blocking IO),即传统的IO模型
    2. 非阻塞式IO( non-blocking IO),默认创建的socket都是阻塞的
    3. IO多路复用(IO multiplexing) ,即经典的Reactor设计模式,有时也称为异步阻塞IO
    4. 异步IO(asynchronous IO),即经典的Proactor设计模式,也称为异步非阻塞IO
    5. 信号驱动式IO(signal driven IO)

    IO操作,主要分为两部分

    1. 数据准备,将数据加载到内核缓存(数据加载到操作系统)
    2. 将内核缓存中的数据加载到用户缓存(从操作系统复制到应用中)

    同步和异步的概念描述的是用户线程与内核的交互方式。
    阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式。

    同步阻塞IO

    同步阻塞IO模型是最简单的IO模型,用户线程在内核进行IO操作时被阻塞


    image.png

    用户线程通过系统调用read发起IO读操作,由用户控件转到内核空间。
    内核等到数据包到达后,然后将接收的数据拷贝到用户空间,完成read操作。

    同步非阻塞IO

    同步阻塞IO是在同步阻塞IO的基础上,将socket设置为NIO(NOBLOCK)。
    这样做用户线程可以在发起IO请求后可以立即返回。


    image.png

    由于socket是非阻塞的方式,因此用户线程发起IO请求时立即返回。
    但未读取到任何数据,用户线程需要不断地发起IO请求,直到数据到达后,才真正读取数据,继续执行。

    IO多路复用

    IO多路复用模型是建立在内核提供的多路分离函数select基础之上的。
    使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。


    image.png

    用户首先将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。
    当数据到达时,socket被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行。

    从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,
    甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。
    但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。
    用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。
    而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

    然而,使用select函数的优点并不仅限于此。
    虽然上述方式允许单线程内处理多个IO请求,但是每个IO请求的过程还是阻塞的(在select函数上阻塞),平均时间甚至比同步阻塞IO模型还要长。
    如果用户线程只注册自己感兴趣的socket或者IO请求,然后去做自己的事情,等到数据到来时再进行处理,则可以提高CPU的利用率。

    IO多路复用模型使用了Reactor设计模式实现了这一机制。

    首先,要从你常用的IO操作谈起,比如read和write,通常IO操作都是阻塞I/O的,也就是说当你调用read时,
    如果没有数据收到,那么线程或者进程就会被挂起,直到收到数据。

    这样,当服务器需要处理1000个连接的的时候,而且只有很少连接忙碌的,
    那么会需要1000个线程或进程来处理1000个连接,而1000个线程大部分是被阻塞起来的。
    由于CPU的核数或超线程数一般都不大,比如4,8,16,32,64,128,比如4个核要跑1000个线程,那么每个线程的时间槽非常短,而线程切换非常频繁。
    这样是有问题的:
    1,线程是有内存开销的,1个线程可能需要512K(或2M)存放栈,那么1000个线程就要512M(或2G)内存。
    2,线程的切换,或者说上下文切换是有CPU开销的,当大量时间花在上下文切换的时候,分配给真正的操作的CPU就要少很多。

    那么,我们就要引入非阻塞I/O的概念,非阻塞IO很简单,通过fcntl(POSIX)或ioctl(Unix)设为非阻塞模式,
    这时,当你调用read时,如果有数据收到,就返回数据,如果没有数据收到,就立刻返回一个错误,如EWOULDBLOCK。
    这样是不会阻塞线程了,但是你还是要不断的轮询来读取或写入。

    于是,我们需要引入IO多路复用的概念。
    多路复用是指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,
    如果有一个文件描述符就绪,则返回,否则阻塞直到超时。
    得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)。

    这样在处理1000个连接时,只需要1个线程监控就绪状态,对就绪的每个连接开一个线程处理就可以了,
    这样需要的线程数大大减少,减少了内存开销和上下文切换的CPU开销。

    异步IO

    “真正”的异步IO需要操作系统更强的支持。
    在IO多路复用模型中,事件循环将文件句柄的状态事件通知给用户线程,由用户线程自行读取数据、处理数据。
    而在异步IO模型中,当用户线程收到通知时,数据已经被内核读取完毕,并放在了用户线程指定的缓冲区内,内核在IO完成后通知用户线程直接使用即可。


    image.png

    异步IO模型使用了Proactor设计模式实现了这一机制。

    相比于IO多路复用模型,异步IO并不十分常用,不少高性能并发服务程序使用IO多路复用模型+多线程任务处理的架构基本可以满足需求。
    况且目前操作系统对异步IO的支持并非特别完善,更多的是采用IO多路复用模型模拟异步IO的方式
    (IO事件触发时不直接通知用户线程,而是将数据读写完毕后放到用户指定的缓冲区中)。
    Java7之后已经支持了异步IO,感兴趣的读者可以尝试使用。

    参考博文:
    https://www.cnblogs.com/straybirds/p/9479158.html

    相关文章

      网友评论

          本文标题:【Java基础】- IO 学习

          本文链接:https://www.haomeiwen.com/subject/qqbsnctx.html