一 网络编程的一些基础概念(重点)
1. 线程的挂起、阻塞、睡眠
线程从创建、运行到结束总是处于下面五个状态之一:新建状
态、就绪状态、运行状态、阻塞状态及死亡状态。下图是使用时间
片轮转法的操作系统进程的状态和它们之间的转换。
挂起和睡眠是主动的,挂起恢复需要主动完成,睡眠恢复则是
自动完成的,因为睡眠有一个睡眠时间,睡眠时间到则恢复到就绪
态。而阻塞是被动的,是在等待某种事件或者资源的表现,一旦获
得所需资源或者事件信息就自动回到就绪态。睡眠和挂起是两种行
为,阻塞则是一种状态。
挂起:一般是主动的,由系统或程序发出,甚至于辅存中去,不释
放cpu。
阻塞:一般是被动的,是放过cpu,不释放内存。在抢占资源中得
不到资源,被动的挂起在内存,等待某种资源或信号量(即
有了资源)将他唤醒。
sleep()是属于阻塞。
关于sleep的说明,如下例子:
class ThreadA extends Thread
{
public void run(){
System.out.println("ThreadA is running");
}
}
public class TestNew {
public static void main(String[] args)throws InterruptedException {
// TODO Auto-generated method stub
ThreadA ta = new ThreadA();
ta.start();
ta.sleep(5000);
System.out.println("TestNew is running");
}
}
其实这段语句是主线程睡眠5秒,而不是ta线程,sleep
声明在哪个线程里哪个线程睡眠
2. 同步/异步
同步和异步关注的是消息通信机制。
(1)同步:就是指在发出一个功能调用时,在没有得到结果之
前,该调用就不返回。按照这个定义,其实绝大多数
函数都是同步调用。
(2)异步:概念和同步相对。 当一个异步过程调用发出后,调用
者不会立刻得到结果。实际处理这个调用的部件是在
调用发出后, 通过状态、通知来通知调用者,或通过
回调函数处理这个调用。
举例:当到银行后, .可以去ATM机前排队等候 – (排队等候)就是同
步等待消息。可以去大厅拿号,等到排到我的号时, 柜台
的人会通知我轮到我去办理业务. – (等待别人通知)就是异步
等待消息。
3. 阻塞/非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时
的状态(无所谓同步或者异步,注意这里的“调用结果”就是指“2”
中标黑的“调用”的结果)
(1)阻塞:阻塞调用是指调用结果返回之前,当前线程会被挂起
(被挂起就是阻塞,因为是被动的)。函数只有在得到
结果之后才会返回。 有人也许会把阻塞调用和同步调
用等同起来,实际上他是不同的。 对于同步调用来说,
很多时候当前线程还是激活的,只是从逻辑上当前函
数没有返回而已。
(2)非阻塞:非阻塞和阻塞的概念相对应,指在不能立刻得到结
果之前,该函数不会阻塞当前线程,而会立刻返回。
举例:继续上面的那个例子,不论是排队等待,还是使用号码等
待通知,如果在这个等待的过程中,等待者除了等待消息
之外不能做其它的事情,那么该机制就是阻塞的,表现在
程序中,也就是该程序一直阻塞在该函数调用处不能继续
往下执行。相反,有的人喜欢在银行办理这些业务的时候
一边打打电话发发短信一边等待,这样的状态就是非阻塞
的。
二 Java的AIO、NIO、BIO(重点)
1. 先看一些详细解释,否则直接看2看不懂
在JDK1.4出来之前,我们建立网络连接的时候采用BIO模式,
需要先在服务端启动一个ServerSocket,然后在客户端启动Socket
来对服务端进行通信,默认情况下服务端需要对每个请求建立一堆
线程等待请求(如果这个连接不做任何事情会造成不必要的线程开
销)而客户端发送请求后,先咨询服务端是否有线程响应,如果没
有则会一直等待或者遭到拒绝请求,如果有的话,客户端会线程会
等待请求结束后才继续执行。
BIO与NIO一个比较重要的不同,是我们使用BIO的时候往往
会引入多线程,每个连接一个单独的线程;而NIO则是使用单线程
或者只使用少量的多线程,每个连接共用一个线程。如下图,上面
为BIO,下面为NIO。
NIO的最重要的地方是当一个连接创建后,不需要对应一个线
程,这个连接会被注册到多路复用器上面,所以所有的连接只需要一
个线程就可以搞定,当这个线程中的多路复用器进行轮询的时候,发
现连接上有请求的话,才开启一个线程进行处理,也就是一个请求
一个线程模式。
在NIO的处理方式中,当一个请求来的话,开启线程进行处理,
可能会等待后端应用的资源(JDBC连接等),其实这个线程就被阻塞
了,当并发上来的话,还是会有BIO一样的问题(不过2中的NIO中
说的,通过注册的回调函数来通知客服端服务器端已经可以处理请
求的方式,避免了这种问题),这时,就进化出了AIO。
HTTP/1.1出现后,有了HTTP长连接,这样除了超时和指明特定
关闭的http header外,这个链接是一直打开的状态的,这样在NIO
处理中可以进一步的进化,在后端资源中可以实现资源池或者队列,
当请求来的话,开启的线程把请求和请求数据传送给后端资源池或
者队列里面就返回,并且在全局的地方保持住这个现场(哪个连接
的哪个请求等),这样前面的线程还是可以去接受其他的请求, 而
后端的应用的处理只需要执行队列里面的就可以了,这样请求处理
和后端应用是异步的(这就是AIO异步的体现)。当后端处理完,
到全局地方得到现场,产生响应,这个就实现了异步处理。
可见:BIO是一个连接一个线程;NIO是一个请求一个线程;AIO是
一个有效请求一个线程。
在举一个烧水的例子来说明一下:
- AIO的做法是,每个水壶上装一个开关,当水开了以后会提醒对
应的线程去处理。
- NIO的做法是,叫一个线程不停的循环观察每一个水壶,根据每
个水壶当前的状态去处理。
- BIO的做法是,叫一个线程停留在一个水壶那,直到这个水壶烧
开,才去处理下一个水壶。
2. 这回在看概念总结
(1)Java BIO : 同步并阻塞,服务器实现模式为一个连接一个线
程,即客户端有连接请求时服务器端就需要启动
一个线程进行处理,如果这个连接不做任何事情
会造成不必要的线程开销,当然可以通过线程池
机制改善。
好理解的解释:线程发起IO请求,不管内核是否
准备好IO操作,从发起请求起,
线程一直阻塞,直到操作完成。
(2)Java NIO : 同步非阻塞,服务器实现模式为一个请求一个线
程,即客户端发送的连接请求都会注册到多路复
用器上,多路复用器轮询到连接有I/O请求时才启
动一个线程进行处理。
好理解的解释:线程(用户空间的哈)发起IO请
求,立即返回;内核在做好IO操
作的准备之后,通过调用注册的
回调函数通知线程做IO操作(IO
操作我自己理解就是将数据拷贝
到用户内存),线程开始阻塞,
直到操作完成。
(3)Java AIO(NIO.2) :异步非阻塞,服务器实现模式为一个有效
请求一个线程,客户端的I/O请求都是由OS
先完成了再通知服务器应用去启动线程进
行处理。
好理解的解释:线程发起IO请求,立即返回;
内核做好IO操作的准备之后,
做IO操作(IO操作我自己理
解就是将数据拷贝到用户内
存),直到操作完成或
者失败,通过调用注册的回
调函数通知线程做IO操作完
成或者失败。
关于AIO、NIO、BIO推荐比较不错的一篇博客
https://www.ibm.com/developerworks/cn/linux/l-async/
有空去阅读
3. BIO、NIO、AIO的使用场景
(1)BIO方式适用于连接数目比较小且固定的架构,这种方式对服务
器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选
择,但程序直观简单易理解。
(2)NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比
如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开
始支持。
(3)AIO方式使用于连接数目多且连接比较长(重操作)的架构,比
如相册服务器,充分调用OS参与并发操作,编程比较复杂,
JDK7开始支持
三 Linux IO模式及 select、poll、epoll详解(了解内容)
注:本节和“四”内容大多摘子博客:
https://segmentfault.com/a/1190000003063859#articleHeader6
https://www.cnblogs.com/zengzy/p/5113910.html(专讲IO多路复用)
本节并为全部摘录,如有时间进行全部阅读。
同步IO和异步IO,阻塞IO和非阻塞IO分别是什么,到底有什么区别?不
同的人在不同的上下文下给出的答案是不同的。所以先限定一下本文的
上下文。
本节讨论的背景是Linux环境下的network IO。
1. 一些概念说明
在解释正题之前需要了解如下一些概念:
- 用户空间和内核空间
- 进程切换
- 进程的阻塞
- 文件描述符
- 缓存 I/O
(1)用户空间与内核空间
现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,
它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统
的核心是内核,独立于普通的应用程序,可以访问受保护的内存
空间,也有访问底层硬件设备的所有权限。为了保证用户进程不
能直接操作内核(kernel),保证内核的安全,操心系统将虚拟
空间划分为两部分,一部分为内核空间,一部分为用户空间。针
对linux操作系统而言,将最高的1G字节(从虚拟地址
0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,
而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),
供各个进程使用,称为用户空间。
(2)进程切换
为了控制进程的执行,内核必须有能力挂起正在CPU上运行
的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进
程切换。因此可以说,任何进程都是在操作系统内核的支持下运
行的,是与内核紧密相关的(所以说线程切换比较消耗资源,它
需要重用户态转换到内核态)。
从一个进程的运行转到另一个进程上运行,这个过程中经过
下面这些变化:
(a)保存处理机上下文,包括程序计数器和其他寄存器。
(b)更新PCB信息。
(c)把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队
列。
(d)选择另一个进程执行,并更新其PCB。
(e)更新内存管理的数据结构。
(f)恢复处理机上下文。
注:总而言之就是很耗资源,具体的可以参考这篇文章:进程切换
(3)进程的阻塞
注:阻塞是线程的一种状态。
正在执行的进程,由于期待的某些事件未发生,如请求系统资
源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,
则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状
态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处
于运行态的进程(获得CPU),才可能将其转为阻塞状态。当进程
进入阻塞状态,是不占用CPU资源的。
(4)文件描述符fd
文件描述符(File descriptor)是计算机科学中的一个术语,是
一个用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一个非负整数。实际上,它是一个索引
值,指向内核为每一个进程所维护的该进程打开文件的记录表。当
程序打开一个现有文件或者创建一个新文件时,内核向进程返回一
个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围
绕着文件描述符展开。但是文件描述符这一概念往往只适用于
UNIX、Linux这样的操作系统。
(5)缓存 I/O
缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都
是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据
缓存在文件系统的页缓存( page cache )中,也就是说,数据会先
被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓
冲区拷贝到应用程序的地址空间。
缓存 I/O 的缺点:
数据在传输过程中需要在应用程序地址空间和内核进行多次数据
拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大
的。
四 IO模式((3)I/O 多路复用( IO multiplexing)其
它均为了解内容)
刚才说了,对于一次IO访问(以read举例),数据会先被拷贝到操
作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用
程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶
段:
- 等待数据准备 (Waiting for the data to be ready)
- 将数据从内核拷贝到用户进程中
(Copying the data from the kernel to the process)
正式因为这两个阶段,linux系统产生了下面五种网络模式的方案。
- 阻塞 I/O(blocking IO)
- 非阻塞 I/O(nonblocking IO)
- I/O 多路复用( IO multiplexing)
- 信号驱动 I/O( signal driven IO)
- 异步 I/O(asynchronous IO)
注:由于signal driven IO在实际中并不常用,所以我这只提及剩下的四
种IO Model。
(1)阻塞 I/O(blocking IO)
在linux中,默认情况下所有的socket都是blocking,一个典
型的读操作流程大概是这样(左边是用户空间,右边是内核空
间):
:
当用户进程调用了recvfrom这个系统调用,kernel(内核)就
开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候
数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。
这个时候kernel就要等待足够的数据到来)。这个过程需要等待,
也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程
的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己
选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从
kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除
block的状态,重新运行起来。
所以,blocking IO的特点就是在IO执行的两个阶段都被
block了。
(2)非阻塞 I/O(nonblocking IO)
linux下,可以通过设置socket使其变为non-blocking。当对一个
non-blocking socket执行读操作时,流程是这个样子(左边是用户
空间,右边是内核空间):
当用户进程发出read操作时,如果kernel中的数据还没有准备好,
那么它并不会block用户进程,而是立刻返回一个error。从用户进程角
度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一
个结果。用户进程判断结果是一个error时,它就知道数据还没有准备
好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,
并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到
了用户内存,然后返回。
所以,nonblocking IO的特点是用户进程需要不断的主动询问
kernel数据好了没有。
(3)I/O 多路复用( IO multiplexing)
IO multiplexing就是我们说的select,poll,epoll,有些地方也称这
种IO方式为event driven IO。select/epoll的好处就在于单个process就
可以同时处理多个网络连接的IO。首先理解两个概念,“多路”和“复用”。
* 多路
指的是多条独立的IO流,IO流可以这么理解:读是一条流(称之
为读流,比如输入流),写是一条流(称之为写流,比如输出流),
异常也是一条流(称之为异常流),每条流用一个文件描述符表示,
同一个文件描述符可以同时表示读流和写流。
* 复用
复用的是线程,复用线程来跟踪每路IO的状态,然后用一个线程
就可以处理所有的IO。
当然,不提什么I/O多路复用也能在一个线程就处理完所有的IO
流,用个while循环挨个处理一次不就解决了嘛?那为什么还要提出
这个技术呢?原因就是刚才我们想的方法(轮询)效率太低了,资
源利用率也不高。试想一下,在单核CPU的情况下,如果设置成了
阻塞IO,那么其他的IO将被卡死,也就浪费掉了其他的IO资源。另
一方面,假设所有IO被设置成非阻塞,那CPU一天到晚也不用干别
的事了,就在这不停的问,现在可以进行IO操作了吗,直到有一个
设备准备好环境才能进行IO,也就是在设备准备io环境的这一段时
间,cpu是没必要瞎问的,问了也没结果。
随后硬件发展起来了,有了多核的概念,也就有了多线程。这个
时候可以这样做,来一条IO我开一个线程,这样的话再也不用轮询了。
然而,管理线程是要耗费系统资源的,程序员也开始头疼了,线程之
间的交互是十分麻烦的。这样一来程序的复杂性蹭蹭蹭地往上涨,IO
效率是可能提高了,但是软件的开发效率却可能减低了。
所以也就有了I/O多路复用这一技术。简单来说,就是一个线程
追踪多条IO流(读,写,异常),但不使用轮询,而是由设备本身
告知程序(select)哪条流可用了,这样一来就解放了CPU,也充
分利用IO资源,下文主要讲解如何实现这一技术,linux下这一技术
有三个实现,select,poll,epoll。今天主要记录自己对select的理
解,从接口到原理再到实现。
select及其原理
* 接口定义
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
struct timeval *timeout);
- readfds,读流集合,也就是程序员希望从这些描述符中读内容
- writefds,写流集合,也就是程序员希望向这些描述符中写内容
- exceptfds,异常流集合,也就是中间过程发送了异常
- nfds,上面三种事件中,最大的文件描述符+1
- timeout,程序员的容忍度,可等待的时间
struct timeval{
long tv_sec;//second
long tv_usec;//minisecond
}
timeout有三种取值:
NULL:select一直阻塞,知道readfds、writefds、exceptfds集合中至少一个文件描述符可用才唤醒
0:select不阻塞
timeout_value:select在timeout_value这个时间段内阻塞
如果非得与“多路”这个词关联起来,那就是readfds+writefds+exceptfds的数量和就是路数。
另外,还有一组与fd_set 有关的操作
FD_SET(fd, _fdset),把fd加入_fdset集合中
FD_CLR(fd, _fdset),把fd从_fdset集合中清除
FD_ISSET(fd, _fdset),判定fd是否在_fdset集合中
FD_ZERO(_fdset),清除_fdset有描述符
* select实现原理
select的实现依赖于设备的驱动函数poll,poll的功能是检
查设备的哪条条流可用(一个设备一般有三条流,读流,写流,
设备发生异常的异常流),如果其中一条流可用,返回一个
mask(表示可用的流),如果不可用,把当前进程加入设备
的流等待队列中,例如读等待队列、写等待队列,并返回资源
不可用。
select正是利用了poll的这个功能,首先让程序员告知自己
关心哪些IO流(用文件描述符表示,也就是上文的readfds、
writefds和exceptfds),并让程序员告知自己这些流的范围
(也就是上文的nfds参数)以及程序的容忍度(timeout参数),
然后select会把她们拷贝到内核,在内核中逐个调用流所对应
的设备的驱动poll函数,当范围内的所有流也就是描述符都遍
历完之后,他会检查是否至少有一个流发生了,如果有,就修
改那三个流集合,把她们清空,然后把发生的流加入到相应的
集合中,并且select返回。如果没有,就睡眠,让出cpu,直
到某个设备的某条流可用,就去唤醒阻塞在流上的进程,这个
时候,调用select的进程重新开始遍历范围内的所有描述符。
直接看这个步骤可能会好理解些
- 拷贝nfds、readfds、writefds和exceptfds到内核
- 遍历[0,nfds)范围内的每个流,调用流所对应的设备的驱动poll
函数
- 检查是否有流发生,如果有发生,把流设置对应的类别,并执
行4,如果没有流发生,执行5。或者timeout=0,执行
- 4select返回
- select阻塞当前进程,等待被流对应的设备唤醒,当被唤醒时,
执行2。或者timeout到期,执行4
select在内核中的流程图
* select实现
int do_select(int n, fd_set_bits *fds, s64 *timeout)
{
retval = 0; //retval用于保存已经准备好的描述符数,初始为0
for (;;) {
unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;
long __timeout;
set_current_state(TASK_INTERRUPTIBLE); //将当前进程状态改为TASK_INTERRUPTIBLE,可中断
inp = fds->in; outp = fds->out; exp = fds->ex;
rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex;
for (i = 0; i < n; ++rinp, ++routp, ++rexp) { //遍历每个描述符
unsigned long in, out, ex, all_bits, bit = 1, mask, j;
unsigned long res_in = 0, res_out = 0, res_ex = 0;
const struct file_operations *f_op = NULL;
struct file *file = NULL;
in = *inp++; out = *outp++; ex = *exp++;
all_bits = in | out | ex;
if (all_bits == 0) {
i += __NFDBITS; //all_bits的类型是unsigned long int ,大小为4个字节32位,all_bits=0,说明连续32个描述符(流)不在readdfs、writedfs、execptdfs集合中,所以i+=32,而__NFDBITS=32。
continue;
}
for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) { //遍历每个长字里的每个位
int fput_needed;
if (i >= n)
break;
if (!(bit & all_bits))
continue;
file = fget_light(i, &fput_needed);
if (file) {
f_op = file->f_op;
MARK(fs_select, "%d %lld", i, (long long)*timeout);
mask = DEFAULT_POLLMASK;
if (f_op && f_op->poll)
mask = (*f_op->poll)(file, retval ? NULL : wait);//调用设备的驱动poll函数
fput_light(file, fput_needed);
if ((mask & POLLIN_SET) && (in & bit)) {
res_in |= bit; //如果是这个描述符可读, 将这个位置位
retval++; //返回描述符个数加1
}
if ((mask & POLLOUT_SET) && (out & bit)) {
res_out |= bit;
retval++;
}
if ((mask & POLLEX_SET) && (ex & bit)) {
res_ex |= bit;
retval++;
}
}
}
if (res_in)
*rinp = res_in;
if (res_out)
*routp = res_out;
if (res_ex)
*rexp = res_ex;
}
wait = NULL;
if (retval || !*timeout || signal_pending(current))//如果retval!=0,也就是有readdfs、writedfs、execptdfs至少有一个发生,跳出循环
break;
/*以下处理timeout参数*/
__timeout = schedule_timeout(__timeout);
if (*timeout >= 0)
*timeout += __timeout;
}
__set_current_state(TASK_RUNNING);
return retval;
}
补充一张IO多路复用的图
下面的内容和上面的内容差不多,只不过来自两篇博客
上图具体过程分大约两个阶段:
第一阶段:当用户进程调用了select,那么整个进程会被block,
而同时,kernel会“监视”所有select负责的socket
(IO流),当任何一个socket(IO流)中的数据准备
好了,select就会返回。
第二阶段:用户进程再调用read操作,将数据从kernel拷贝到用
户进程。
所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待
多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一
个进入读就绪状态,select()函数就可以返回。
这个图和blocking IO的图其实并没有太大的不同,事实上,还更
差一些。因为这里需要使用两个system call (select 和 recvfrom),而
blocking IO只调用了一个system call (recvfrom)。但是,用select的
优势在于它可以同时处理多个connection。
所以,如果处理的连接数不是很高的话,使用select/epoll的
web server不一定比使用multi-threading + blocking IO的
web server性能更好,可能延迟还更大。select/epoll的优势并不是对
于单个连接能处理得更快,而是在于能处理更多的连接。
在IO multiplexing Model中,实际中,对于每一个socket,一般
都设置成为non-blocking,但是,如上图所示,整个用户的process其
实是一直被block的。只不过process是被select这个函数block,而不
是被socket IO给block。
(4)异步 I/O(asynchronous IO,AIO)
Linux下的asynchronous IO其实用得很少。先看一下它的流程:
用户进程发起read操作之后,立刻就可以开始去做其它的事。而
另一方面,从kernel的角度,当它受到一个asynchronous read之后,
首先它会立刻返回,所以不会对用户进程产生任何block。然后,
kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切
都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完
成了。
一. 网络编程的一些基础
1.先说明一下线程的挂起、阻塞、睡眠
线程从创建、运行到结束总是处于下面五个状态之一:新建状态、就绪状态、运行状
态、阻塞状态及死亡状态。下图是使用时间片轮转法的操作系统进程的状态和它们之
间的转换。
挂起和睡眠是主动的,挂起恢复需要主动完成,睡眠恢复则是自动完成的,因为
睡眠有一个睡眠时间,睡眠时间到则恢复到就绪态。而阻塞是被动的,是在等待某种
事件或者资源的表现,一旦获得所需资源或者事件信息就自动回到就绪态。睡眠和挂
起是两种行为,阻塞则是一种状态。
挂起:一般是主动的,由系统或程序发出,甚至于辅存中去join()、wait()是挂起。
阻塞:一般是被动的,在抢占资源中得不到资源,被动的挂起在内存,等待某种资
源或信号量(即有了资源)将他唤醒。sleep()是属于阻塞。
2.同步/异步
同步和异步关注的是消息通信机制。
(1)同步:就是指在发出一个功能调用时,在没有得到结果之前,该调用就不返回。
按照这个定义,其实绝大多数函数都是同步调用。
(2)异步:概念和同步相对。 当一个异步过程调用发出后,调用者不会立刻得到结
果。实际处理这个调用的部件是在调用发出后, 通过状态、通知来通知
调用者,或通过回调函数处理这个调用。
举例:当到银行后, .可以去ATM机前排队等候 – (排队等候)就是同步等待消息 .可以去
大厅拿号,等到排到我的号时, 柜台的人会通知我轮到我去办理业务. – (等待别
人通知)就是异步等待消息。
3.阻塞/非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态(无所谓同
步或者异步,注意这里的“调用结果”就是指“2”中标黑的“调用”的结果)
(1)阻塞:阻塞调用是指调用结果返回之前,当前线程会被挂起(被挂起就是阻塞,
因为是被动的)。函数只有在得到结果之后才会返回。 有人也许会把阻
塞调用和同步调用等同起来,实际上他是不同的。 对于同步调用来说,
很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。
(2)非阻塞:非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不
会阻塞当前线程,而会立刻返回。
举例:继续上面的那个例子,不论是排队等待,还是使用号码等待通知,如果在这个
等待的过程中,等待者除了等待消息之外不能做其它的事情,那么该机制就是
阻塞的,表现在程序中,也就是该程序一直阻塞在该函数调用处不能继续往下
执行。相反,有的人喜欢在银行办理这些业务的时候一边打打电话发发短信一
边等待,这样的状态就是非阻塞的。
二. Java BIO、NIO、AIO
1.先看一些详细解释,否则直接看2看不懂
在JDK1.4出来之前,我们建立网络连接的时候采用BIO模式,需要先在服务端启动一
个ServerSocket,然后在客户端启动Socket来对服务端进行通信,默认情况下服务端
需要对每个请求建立一堆线程等待请求(如果这个连接不做任何事情会造成不必要的
线程开销)而客户端发送请求后,先咨询服务端是否有线程响应,如果没有则会一直
等待或者遭到拒绝请求,如果有的话,客户端会线程会等待请求结束后才继续执行。
BIO与NIO一个比较重要的不同,是我们使用BIO的时候往往会引入多线程,每个
连接一个单独的线程;而NIO则是使用单线程或者只使用少量的多线程,每个连接共用
一个线程。如下图,上面为BIO,下面为NIO。
NIO的最重要的地方是当一个连接创建后,不需要对应一个线程,这个连接会被注
册到多路复用器上面,所以所有的连接只需要一个线程就可以搞定,当这个线程中的
多路复用器进行轮询的时候,发现连接上有请求的话,才开启一个线程进行处理,也就
是一个请求一个线程模式。
在NIO的处理方式中,当一个请求来的话,开启线程进行处理,可能会等待后端应
用的资源(JDBC连接等),其实这个线程就被阻塞了,当并发上来的话,还是会有BIO一
样的问题(不过2中的NIO中说的,通过注册的回调函数来通知客服端服务器端已经可以
处理请求的方式,避免了这种问题),这时,就进化出了AIO。
HTTP/1.1出现后,有了Http长连接,这样除了超时和指明特定关闭的http header外,
这个链接是一直打开的状态的,这样在NIO处理中可以进一步的进化,在后端资源中可
以实现资源池或者队列,当请求来的话,开启的线程把请求和请求数据传送给后端资源
池或者队列里面就返回,并且在全局的地方保持住这个现场(哪个连接的哪个请求等),
这样前面的线程还是可以去接受其他的请求, 而后端的应用的处理只需要执行队列里面
的就可以了,这样请求处理和后端应用是异步的(这就是AIO异步的体现)。当后端处
理完,到全局地方得到现场,产生响应,这个就实现了异步处理。
可见:BIO是一个连接一个线程;NIO是一个请求一个线程;AIO是一个有效请求一个线
程。
在举一个烧水的例子来说明一下:
* AIO的做法是,每个水壶上装一个开关,当水开了以后会提醒对应的线程去处理。
* NIO的做法是,叫一个线程不停的循环观察每一个水壶,根据每个水壶当前的状态去处
理。
* BIO的做法是,叫一个线程停留在一个水壶那,直到这个水壶烧开,才去处理下一个水
壶。
2.这回在看概念总结
(1)Java BIO : 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连
接请求时服务器端就需要启动一个线程进行处理,如果这个连接不
做任何事情会造成不必要的线程开销,当然可以通过线程池机制改
善。
好理解的解释:线程发起IO请求,不管内核是否准备好IO操作,从
发起请求起,线程一直阻塞,直到操作完成。
(2)Java NIO : 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送
的连接请求都会注册到多路复用器上,多路复用器轮询到连接有
I/O请求时才启动一个线程进行处理。
好理解的解释:线程发起IO请求,立即返回;内核在做好IO操作的
准备之后,通过调用注册的回调函数通知线程做IO
操作,线程开始阻塞,直到操作完成。
(3)Java AIO(NIO.2) : 异步非阻塞,服务器实现模式为一个有效请求一个线程,客
户端的I/O请求都是由OS先完成了再通知服务器应用去启动
线程进行处理。
好理解的解释:线程发起IO请求,立即返回;内存做好IO操
作的准备之后,做IO操作,直到操作完成或
者失败,通过调用注册的回调函数通知线程
做IO操作完成或者失败。
2. BIO、NIO、AIO的使用场景
(1)BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比
较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
(2)NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,
并发局限于应用中,编程比较复杂,JDK1.4开始支持。
(3)AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,
充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。