JAVA 的IO

IO里面的常见类,字节流、字符流、接口、实现类、 方法阻塞
 流有输入和输出,输入时是流从数据源流向程序。输出时是流从程序传向数据源,而数据源可以是内 存,文件,网络或程序等。 

1.输入流和输出流 
根据数据流向不同分为:输入流和输出流。
2.字节流和字符流 
字节流和字符流和用法几乎完全一样,区别在于字节流和字符流所操作的数据单元不同。 字符流的由来: 因为数据编码的不同,而有了对字符进行高效操作的流对象。本质其实就是基于字节流 读取时,去查了指定的码表。字节流和字符流的区别:

(1)读写单位不同:字节流以字节(8bit)为单位,字符流以字符为单位,根据码表映射字符,一次 可能读多个字节。
 (2)处理对象不同:字节流能处理所有类型的数据(如图片、avi等),而字符流只能处理字符类型的 数据。
只要是处理纯文本数据,就优先考虑使用字符流。 除此之外都使用字节流。
 

3.节点流和处理流 
按照流的角色来分,可以分为节点流和处理流。 可以从/向一个特定的IO设备(如磁盘、网络)读/写数据的流,称为节点流,节点流也被成为低级流。 处理流是对一个已存在的流进行连接或封装,通过封装后的流来实现数据读/写功能,处理流也被称为 高级流。

在执行完流操作后,要调用 close() 方法来关系输入流,因为程序里打开的IO资源不属于内存资源,垃 圾回收机制无法回收该资源,所以应该显式关闭文件IO资源。

使用Java的IO流执行输出时,不要忘记关闭输出流,关闭输出流除了可以保证流的物理资源被回收之 外,还能将输出流缓冲区的数据flush到物理节点里(因为在执行close()方法之前,自动执行输出流的 flush()方法)

NIO

NIO技术概览 
NIO(Non-blocking I/O,在Java领域,也称为New I/O),是一种同步非阻塞的I/O模型,也是I/O多路 复用的基础,已经被越来越多地应用到大型应用服务器,成为解决高并发与大量连接、I/O处理问题的有 效方式。 

IO模型的分类 
按照《Unix网络编程》的划分,

I/O模型可以分为:

阻塞I/O模型、非阻塞I/O模型、I/O复用模型、信号 驱动式I/O模型和异步I/O模型

按照POSIX标准来划分只分为两类:同步I/O和异步I/O
如何区分呢?首先一个I/O操作其实分成了两个步骤:发起IO请求和实际的IO操作

同步I/O和异步I/O的 区别就在于第二个步骤是否阻塞,

如果实际的I/O读写阻塞请求进程,那么就是同步I/O,因此阻塞I/O、 非阻塞I/O、I/O复用、信号驱动I/O都是同步I/O,

如果不阻塞,而是操作系统帮你做完I/O操作再将结果 返回给你,那么就是异步I/O。

阻塞I/O和非阻塞I/O的区别在于第一步,

发起I/O请求是否会被阻塞,如果阻塞直到完成那么就是传统的 阻塞I/O,如果不阻塞,那么就是非阻塞I/O。 

阻塞I/O模型 :在linux中,默认情况下所有的socket都是blocking.(等待返回结果)

非阻塞I/O模型:linux下,可以通过设置socket使其变为non-blocking.(不到询问是否有数据可读)

I/O复用模型:我们可以调用 select 或 poll或epoll ,阻塞在这两个系统调用中的某一个之上,而不是真 正的IO系统调用上。

信号驱动式I/O模型:我们可以用信号,让内核在描述符就绪时发送SIGIO信号通知我们

异步I/O模型:用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从内核 的角度,当它受到一个asynchronousread之后,首先它会立刻返回,所以不会对用户进程产生任 何block。然后,内核会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后, 内核会给用户进程发送一个signal,告诉它read操作完成了:

阻塞模型需要在 I/O 操作开始时阻塞应用程序。 这意味着不可能同时重叠进行处理和 I/O 操作。非阻塞模型允许处理和 I/O 操作重叠进行,但是这需要 应用程序来检查 I/O 操作的状态。对于异步I/O ,它允许处理和 I/O 操作重叠进行,包括 I/O 操作完成的 通知。除了需要阻塞之外,select 函数所提供的功能(异步阻塞 I/O)与 AIO 类似。不过,它是对通知 事件进行阻塞,而不是对 I/O 调用进行阻塞。

两种IO多路复用方案:Reactor和Proactor 
一般地,I/O多路复用机制都依赖于一个事件多路分离器(Event Demultiplexer)。分离器对象可将来自 事件源的I/O事件分离出来,并分发到对应的read/write事件处理器(Event Handler)。开发人员预先注 册需要处理的事件及其事件处理器(或回调函数);事件分离器负责将请求事件传递给事件处理器。
两个与事件分离器有关的模式是Reactor和Proactor。

Reactor模式采用同步I/O,而Proactor采用异步 I/O。

在Reactor中,事件分离器负责等待文件描述符或socket为读写操作准备就绪,然后将就绪事件传 递给对应的处理器,后由处理器负责完成实际的读写工作。
而在Proactor模式中,处理器或者兼任处理器的事件分离器,只负责发起异步读写操作。I/O操作本身由 操作系统来完成。传递给操作系统的参数需要包括用户定义的数据缓冲区地址和数据大小,操作系统才 能从中得到写出操作所需数据,或写入从socket读到的数据。事件分离器捕获I/O操作完成事件,然后将 事件传递给对应处理器。比如,在windows上,处理器发起一个异步I/O操作,再由事件分离器等待IOCompletion事件。典型的异步模式实现,都建立在操作系统支持异步API的基础之上,我们将这种实 现称为“系统级”异步或“真”异步,因为应用程序完全依赖操作系统执行真正的I/O工作。 

在Reactor中实现读:
注册读就绪事件和相应的事件处理器;

事件分离器等待事件;

事件到来,激活分离器,分离器调用事件对应的处理器;

事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权。


在Proactor中实现读:

处理器发起异步读操作(注意:操作系统必须支持异步I/O)。

在这种情况下,处理器无视I/O就绪 事件,它关注的是完成事件;

事件分离器等待操作完成事件;

在分离器等待过程中,操作系统利用并行的内核线程执行实际的读操作,并将结果数据存入用户自 定义缓冲区,后通知事件分离器读操作完成;

事件分离器呼唤处理器; 事件处理器处理用户自定义缓冲区中的数据,然后启动一个新的异步操作,并将控制权返回事件分 离器。
可以看出,两个模式的相同点,都是对某个I/O事件的事件通知(即告诉某个模块,这个I/O操作可以进 行或已经完成)。在结构上,两者的相同点和不同点如下:
相同点:demultiplexor负责提交I/O操作(异步)、查询设备是否可操作(同步),然后当条件满 足时,就回调handler; 不同点:异步情况下(Proactor),当回调handler时,表示I/O操作已经完成;同步情况下 (Reactor),回调handler时,表示I/O设备可以进行某个操作(can read or can write)。

传统BIO模型 :一个连接一个线程,要开启多线程。

这里之所以使用多线程,是因为socket.accept()、inputStream.read()、outputStream.write()都是同步 阻塞的,当一个连接在处理I/O的时候,系统是阻塞的,如果是单线程的话在阻塞的期间不能接受任何请 求。所以,使用多线程,就可以让CPU去处理更多的事情。其实这也是所有使用多线程的本质:
利用多核。
 
当I/O阻塞系统,但CPU空闲的时候,可以利用多线程使用CPU资源。 使用线程池能够让线程的创建和回收成本相对较低。在活动连接数不是特别高(小于单机1000)的情况 下,这种模型是比较不错的,可以让每一个连接专注于自己的I/O并且编程模型简单,也不用过多考虑系 统的过载、限流等问题。线程池可以缓冲一些过多的连接或请求。
但这个模型本质的问题在于,严重依赖于线程。但线程是很”贵”的资源,主要表现在:

1. 线程的创建和销毁成本很高,在Linux这样的操作系统中,线程本质上就是一个进程。创建和销毁 都是重量级的系统函数;

2. 线程本身占用较大内存,像Java的线程栈,一般至少分配512K~1M的空间,如果系统中的线程数 过千,恐怕整个JVM的内存都会被吃掉一半;

3. 线程的切换成本是很高的。操作系统发生线程切换的时候,需要保留线程的上下文,然后执行系统 调用。如果线程数过高,可能执行线程切换的时间甚至会大于线程执行的时间,这时候带来的表现 往往是系统load偏高、CPU sy使用率特别高(超过20%以上),导致系统几乎陷入不可用的状态;

4. 容易造成锯齿状的系统负载。因为系统负载是用活动线程数或CPU核心数,一旦线程数量高但外部 网络环境不是很稳定,就很容易造成大量请求的结果同时返回,激活大量阻塞线程从而使系统负载 压力过大。

NIO基于Reactor,当socket有流可读或可写入socket时,操作系统会相应的通知应用程序进行处理,应 用再将流读取到缓冲区或写入操作系统。
也就是说,这个时候,已经不是一个连接就要对应一个处理线程了,而是有效的请求,对应一个线程, 当连接没有数据时,是没有工作线程来处理的。

 

1.Acceptor注册Selector,监听accept事件;

2.当客户端连接后,触发accept事件;

3.服务器构建对应的Channel,并在其上注册Selector,监听读写事件;

4.当发生读写事件后,进行相应的读写处理

Reactor模型 

Reactor单线程模型

Reactor多线程模型 
相比上一种模型,该模型在处理器链部分采用了多线程(线程池):

 

AIO 
与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可。这两种方法均为异步的, 对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序; 对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。 即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。 在JDK1.7中,这部分内容被称作NIO.2,主要在java.nio.channels包下增加了下面四个异步通道: AsynchronousSocketChannel AsynchronousServerSocketChannel AsynchronousFileChannel AsynchronousDatagramChannel

select和epoll的区别 
当需要读两个以上的I/O的时候,如果使用阻塞式的I/O,那么可能长时间的阻塞在一个描述符上面,另 外的描述符虽然有数据但是不能读出来,这样实时性不能满足要求,大概的解决方案有以下几种:
1. 使用多进程或者多线程,但是这种方法会造成程序的复杂,而且对与进程与线程的创建维护也需要 很多的开销(Apache服务器是用的子进程的方式,优点可以隔离用户);

2. 用一个进程,但是使用非阻塞的I/O读取数据,当一个I/O不可读的时候立刻返回,检查下一个是否 可读,这种形式的循环为轮询(polling),这种方法比较浪费CPU时间,因为大多数时间是不可 读,但是仍花费时间不断反复执行read系统调用;

3. 异步I/O,当一个描述符备好的时候用一个信号告诉进程,但是由于信号个数有限,多个描述符 时不适用;

4. 一种较好的方式为I/O多路复用,先构造一张有关描述符的列表(epoll中为队列,哈希),然后调用一个 函数,直到这些描述符中的一个准备好时才返回,返回时告诉进程哪些I/O就绪。select和epoll这 两个机制都是多路I/O机制的解决方案,select为POSIX标准中的,而epoll为Linux所特有的。

它们的区别主要有三点:
1. select的句柄数目受限,在linux/posix_types.h头文件有这样的声明: #define __FD_SETSIZE 1024 表示select多同时监听1024个fd。而epoll没有,它的限制是大的打开文件句柄数目;

2. epoll的大好处是不会随着FD的数目增长而降低效率,在selec中采用轮询处理,其中的数据结构 类似一个数组的数据结构,而epoll是维护一个队列,直接看队列是不是空就可以了。epoll只会对” 活跃”的socket进行操作—这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那 么,只有”活跃”的socket才会主动的去调用 callback函数(把这个句柄加入队列),其他idle状态 句柄则不会,在这点上,epoll实现了一个”伪”AIO但是如果绝大部分的I/O都是“活跃的”,每个I/O 端口使用率很高的话,epoll效率不一定比select高(可能是要维护队列复杂);

3. 使用mmap加速内核与用户空间的消息传递。无论是select,poll还是epoll都需要内核把FD消息通知 给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核于用户空间 mmap同一块内存实现的。

NIO与epoll 
上文说到了select与epoll的区别,再总结一下Java NIO与select和epoll: Linux2.6之后支持epoll windows支持select而不支持epoll 不同系统下nio的实现是不一样的,包括Sunos linux 和windows select的复杂度为O(N) select有大fd限制,默认为1024 修改sys/select.h可以改变select的fd数量限制 epoll的事件模型,无fd数量限制,复杂度O(1),不需要遍历fd


 

 

 

 

Zero Copy 
许多web应用都会向用户提供大量的静态内容,这意味着有很多数据从硬盘读出之后,会原封不动的通 过socket传输给用户。 这种操作看起来可能不会怎么消耗CPU,但是实际上它是低效的:
1. kernel把从disk读数据; 2. 将数据传输给application; 3. application再次把同样的内容再传回给处于kernel级的socket。 这种场景下,application实际上只是作为一种低效的中间介质,用来把磁盘文件的数据传给socket。
数据每次传输都会经过user和kernel空间都会被copy,这会消耗cpu,并且占用RAM的带宽。
传统的数据传输方式 
像这种从文件读取数据然后将数据通过网络传输给其他的程序的方式其核心操作就是如下两个调用:
其上操作看上去只有两个简单的调用,但是其内部过程却要经历四次用户态和内核态的切换以及四次的 数据复制操作:
 

 

 

使用transferTo()方式所经历的步骤:
1. transferTo调用会引起DMA将文件内容复制到读缓冲区(内核空间的缓冲区),然后数据从这个缓冲 区复制到另一个与socket输出相关的内核缓冲区中;
public abstract long transferTo(long position, long count,                                    WritableByteChannel target)        throws IOException;
2. 第三次数据复制就是DMA把socket关联的缓冲区中的数据复制到协议引擎上发送到网络上。
这次改善,我们是通过将内核、用户态切换的次数从四次减少到两次,将数据的复制次数从四次减少到 三次(只有一次用到cpu资源)。但这并没有达到我们零复制的目标。如果底层网络适配器支持收集操作的 话,我们可以进一步减少内核对数据的复制次数。在内核为2.4或者以上版本的linux系统上,socket缓 冲区描述符将被用来满足这个需求。这个方式不仅减少了内核用户态间的切换,而且也省去了那次需要 cpu参与的复制过程。从用户角度来看依旧是调用transferTo()方法,但是其本质发生了变化:
1. 调用transferTo方法后数据被DMA从文件复制到了内核的一个缓冲区中;

2. 数据不再被复制到socket关联的缓冲区中了仅仅是将一个描述符(包含了数据的位置和长度等信 息)追加到socket关联的缓冲区中。DMA直接将内核中的缓冲区中的数据传输给协议引擎,消除了 仅剩的一次需要cpu周期的数据复制

 

NIO存在的问题 
使用NIO != 高性能,当连接数<1000,并发程度不高或者局域网环境下NIO并没有显著的性能优势。
NIO并没有完全屏蔽平台差异,它仍然是基于各个操作系统的I/O系统实现的,差异仍然存在。使用NIO 做网络编程构建事件驱动模型并不容易,陷阱重重。
推荐使用成熟的NIO框架,如Netty,MINA等。解决了很多NIO的陷阱,并屏蔽了操作系统的差 异,有较好的性能和编程模型。

总结 
后总结一下NIO有哪些优势:
事件驱动模型

避免多线程 单线程处理多任务

非阻塞I/O,I/O读写不再阻塞 基于block的传输,通常比基于流的传输更高效 更高级的IO函数,Zero Copy I/O多路复用大大提高了Java网络应用的可伸缩性和实用性
 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值