高并发引起的问题
一个使用传统阻塞I/O的系统,如果还是使用传统的一个请求对应一个线程这种模式,一旦有高并发的大量请求,就会有如下问题:
- 线程不够用, 就算使用了线程池复用线程也无济于事;
- 阻塞I/O模式下,会有大量的线程被阻塞,一直在等待数据,这个时候的线程被挂起,只能干等,CPU利用率很低,换句话说,系统的吞吐量差;
- 如果网络I/O堵塞或者有网络抖动或者网络故障等,线程的阻塞时间可能很长。整个系统也变的不可靠;
概述
- 指JDK1.4及以上版本里提供的新的API,为所有原始类型(boolean类型除外)提供缓存支持的数据容器,使用它可以提供非阻塞式的高伸缩性网络。
- NIO的作用:进行数据传输
- NIO的组件:Buffer,Channel,Selector
1
2
3
4// IO的分类
BIO - BlockingIO - 同步阻塞式IO
NIO - NewIO - NonBlockingIO - 同步非阻塞IO - JDK1.4
AIO - AsynchronousIO - 异步非阻塞式IO - JDK1.8
阻塞与同步
同步:
所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。也就是必须一件一件事做,等前一件做完了才能做下一件事。
例如普通B/S模式(同步):提交请求->等待服务器处理->处理完毕返回 这个期间客户端浏览器不能干任何事
异步:
异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。
例如 ajax请求(异步): 请求通过事件触发->服务器处理(这是浏览器仍然可以作其他事情)->处理完毕
阻塞:
阻塞调用是指调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,cpu不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之后才会返回。
有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回,它还会抢占cpu去执行其他逻辑,也会主动检测io是否准备好。
非阻塞
非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。
再简单点理解就是:
- 同步:就是我调用一个功能,该功能没有结束前,我死等结果。
- 异步:就是我调用一个功能,不需要知道该功能结果,该功能有结果后通知我(回调通知)
- 阻塞:就是调用我(函数),我(函数)没有接收完数据或者没有得到结果之前,我不会返回。
- 非阻塞:就是调用我(函数),我(函数)立即返回,通过select通知调用者
同步IO和异步IO的区别就在于:数据拷贝的时候进程是否阻塞
阻塞IO和非阻塞IO的区别就在于:应用程序的调用是否立即返回
综上可知:
同步和异步,阻塞和非阻塞,有些混用,其实它们完全不是一回事,而且它们修饰的对象也不相同。
BIO与NIO对比
IO模型 | BIO | NIO |
---|---|---|
方式 | 从硬盘到内存 | 从内存到硬盘 |
通信 | 面向流 | 面向缓存 |
处理 | 阻塞式IO | 非阻塞IO |
触发 | 无 | 选择器(轮询机制) |
- BIO缺点
a.阻塞:导致效率整体降低。
b.一对一连接:客户端每过来一个请求,那么在服务端就需要一个线程去处理这个请求。如果客户端产生大量的请求,会导致服务端也产生大量的线程去处理请求,服务器端的线程数量一旦过多,会导致服务器的卡顿甚至崩溃。
c.如果客户端连接之后不产生任何操作,依然会占用服务端的线程,导致服务器资源的浪费。 - 面向流与面向缓冲
Java NIO和IO之间第一个最大的区别是,IO是面向流的.NIO是面向缓冲区的。Java IO面向流意味着毎次从流中读一个成多个字节,直至读取所有字节,它们没有被缓存在任何地方,此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的教据,需要先将它缓存到一个缓冲区。Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,霱要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数裾。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
Buffer - 缓存区
- 作用存储数据
- 容器:数组,针对基本类型提供了其中对应的Buffer(boolean类型除外)
- 重要位置: capacity >= limit >= position >= mark
容量(Capacity):缓冲区能够容纳的数据元素的最大数量。这一个容量在缓冲区创建时被设定,并且永远不能改变。
上界(Limit):限制位。用于限制操作位所能达到的最大位置。缓冲区刚创建时,limit和capacity重合
位置(Position):下一个要被读或写的元素的索引。用于执行要读写的位置。默认pisition为0。
标记(Mark):下一个要被读或写的元素的索引。类似于数组的下标,用于执行要读写的位置。默认情况下mark为-1 - 重要操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// flip:翻转缓冲区
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
// clear:清空缓冲区
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
// reset:重置缓冲区
public final Buffer rewind() {
postion = 0;
mark = -1;
return this;
}
Channel - 通道
- 作用:传输数据。
- 分类:
a.文件 - FileChannel
b.UDP - DatagramChannel
c.TCP - SocketChannel、ServerSocketChannel - 通道默认时阻塞的,可以手动设置为非阻塞。
- 通道可以进行双向传输。
Selector - 选择器
Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通里已经有可以处理的褕入,或者选择已准备写入的通道。这选怿机制,使得一个单独的线程很容易来管理多个通道。
- 作用:选择事件。
- 选择器是面向通道进行操作,但是选择器要求通道必须是非阻塞的。
- 利用选择器,可以实现一对多的效果。