同步、异步与阻塞的关联
1、相关概念:
同步与异步;阻塞与非阻塞本质上是一个相对的概念
进程通信层面:同步与异步针对发送方而言,取决于数据写到内核缓存区进程的行为,继续等待视为同步,否则视为异步
进程通信层面:阻塞非阻塞针对的是接收方而言,取决于将内核中的数据能否立即拷贝到用户空间,如果不能直接拷贝则为阻塞,反之则为非阻塞。
阻塞是进程的一种状态,cpu远高于磁盘速度,为了提高cpu利用率,凡是涉及系统调用的进程(牵扯到磁盘读写),会把进程置为阻塞状态,防止cpu调度。
1.1、前置知识:
- 用户空间和内核空间:
- os支持多进程同时运行,但需要保证不同进程之间不会相互影响(一个进程不能恶意读取或修改其他进程中的数据)
- 内存空间:用户空间和内核空间
- 用户空间:数据处理
- 内核空间:数据的读写及其他的调度和管理
- 进程通信:
- 两进程间通信时,会将数据从用户空间的缓冲区拷贝到内核空间,然后将内核空间缓冲区的数据发送出去
- 接收时,先把网络传输过来的数据放置到内核缓冲区中,然后再拷贝到用户空间缓冲区中进行处理。
1.2、同步与异步:
主要针对发送者,也就是服务调用者,针对调用者获取结果的方式
针对应用程序和内核的交互而言,OS层面
同步:用户进程触发io操作并等待或者轮询去查看这个IO操作是否有绪
异步:用户进程触发io操作之后去做其他事情,而当IO操作完成时会得到IO完成的通知
- 同步:你发起了请求(触发IO操作)此时你必须等待对方给你返回结果,你不能去干其他的事情(你发送了你等),返回结果之前不能干其他事情(synchronizedz中wait,notify/notifyAll)
- 异步:你发送了请求(触发操作)此时你没必要等待对方给你返回结果,你可以直接返回做自己的事,等待处理完成后给你返回处理完成的标志(你发送了你不等),结果:我通过状态、通知、回调函数来告诉你
- CountDownLatch
- await:latch的计数如果大于0,则线程会被挂起,否则不会
- countDown:在其他线程回调返回结果的地方用latch.countDown( )来释放线程,当latch的初始值为大于1时,latch需要等所有的结果返回后被countDown( ) 为0后挂起的线程才会继续执行)
- 场景:多个结果需要同步才唤醒线程的场景(多个文件下载,下载完成之后才能后续处理)
- CountDownLatch
1.3、阻塞与非阻塞:
针对被调用者(接收者)、服务提供者,等待调用结果这段时间线程的状态
OS层面:针对于进程访问数据时候,根据IO操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作函数的实现方式
阻塞:读取/写入函数一直等待
非阻塞:读取或者写入函数会立即返回一个状态值。
- 阻塞:当你调用我的时候,我试图对文件进行读写的时候发现没有可读写的文件,此时我就进入了等待状态(当前线程被挂起),等可以读写的时候,我处理完给你返回结果,(用户调用我订单服务,我订单服务不可用,我等),调用结果返回之前,当前线程被挂起
- 非阻塞:当你调用我的时候,我试图对文件进行读写的时候发现没有可读写的文件,此时我不等待直接返回,等我发现可以读写文件处理完成再给你返回成功标志,(用户调用我订单服务,我订单服务不可用,我不等,处理完了给你返回结果),调用结果返回之前,当前线程不被挂起,继续执行
二、同步阻塞、同步非阻塞:
2.1、同步阻塞:
OS层面:用户进程发起一个IO操作,必须等待IO操作的完成,只有当真正完成了IO操作以后,用户进程才会运行(JAVA传统IO模型)
简介:
- 英文简称为BIO
- jdk1.4之前
- 面向流(基于字节流和字符流进行操作),读写数据都是单向的
- 传统的java io编程
- 你调用我,等待我给你返回结果,我发现此时没有可读写的资源我也等待(被挂起),两者一起等,等我处理完之后再给你返回结果
- 相关的类和接口在java.io包中
- 服务区实现模式:一个连接一个线程,即客户端有连接请求时,服务端就需要启动一个线程来进行处理,若此时这个连接不做任何处理会造成不必要的线程开销,可以通过线程池机制改进。
工作机制:
- 客户端:
- 通过socket对象请求与服务器建立连接
- 从sockey中得到字节输入或者字节输出流进行数据读写操作。
- 服务端:
- 通过ServerSocket注册端口,绑定IP地址
- 服务端调用accept方法用于监听客户端的socket请求
- 从socket中得到字节输入或者字节输出流进行数据读写操作
代码实现:
服务端:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerSocketDemo {
public static void main(String[] args) {
System.out.println("==========服务端启动==========");
ServerSocket serverSocket=null;
try {
serverSocket=new ServerSocket(8889);
//监听客户端的Socket连接请求
Socket socket = serverSocket.accept();
InputStream is = socket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
String msg;
/*if((msg=reader.readLine())!=null){
System.out.println("服务端接收客户端信息为:" + msg);
}*/
while ((msg=reader.readLine())!=null){
System.out.println("服务端接收客户端信息为:" + msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端:
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
public class Client {
public static void main(String[] args) {
Socket socket=null;
try {
socket=new Socket("127.0.0.1",8889);
OutputStream stream = socket.getOutputStream();
PrintStream ps = new PrintStream(stream);
Scanner scanner = new Scanner(System.in);
while (true){
System.out.println("请输入:");
String s = scanner.nextLine();
ps.println(s);
ps.flush();
}
// ps.println("hello World! 与服务端通信成功");
//ps.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
特点:
- BIO通信中,服务端一直等待客户端的消息,客户端没有进行消息的发送,服务器一直阻塞
- 服务器按行获取信息,客户端也必须按行进行消息的发送,否则服务端进入等待消息的阻塞状态。
BIO模拟客户端服务端多对一:
//ClientTest03 \02\01
public class ClientTest {
public static void main(String[] args) {
try {
Socket socket = new Socket("localhost",8889);
OutputStream os = socket.getOutputStream();
PrintStream ps = new PrintStream(os);
Scanner scanner = new Scanner(System.in);
while (true){
System.out.println("请输入:");
String s = scanner.nextLine();
ps.println(s);
ps.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
//服务端
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerTest {
public static void main(String[] args) {
ServerSocket serverSocket=null;
try {
System.out.println("服务端开始-------------------------");
serverSocket= new ServerSocket(8889);
while (true){
Socket socket = serverSocket.accept();
new Thread(new ServerReader(socket)).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 一个连接一个线程
*/
static class ServerReader implements Runnable{
public Socket socket;
public ServerReader() {
}
public ServerReader(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
InputStream is = socket.getInputStream();
InputStreamReader streamReader = new InputStreamReader(is);
BufferedReader reader = new BufferedReader(streamReader);
String msg;
while ((msg=reader.readLine())!=null){
System.out.println("当前线程名称"+Thread.currentThread().getName()+",,,服务端接收到:" + msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
总结:
- 一个连接(客户端发起)一个线程(服务端创建),但线程的竞争、切换上下文影响性能;
- 每个线程会占用栈空间和cpu资源
- 并不是每个socket都进行lO操作
- 客户端并发访问增加,服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出, 线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。
- 一般在BIO中会采用多线程
2.2、同步非阻塞:
jdk1.4之前采用BIO方式,一个连接一个线程,先在服务端启动一个ServerSocket(绑定端口),然后在客户端启动Socket来对此服务端进行通信,默认情况下服务端需要对每个请求建立一堆线程等待请求,而客户端发送请求后,先咨询服务端是否有线程相应,如果没有则会一直等待或者遭到拒绝请求,如果有的话,客户端等待请求结束后才继续执行。
同步I/O的网络应用中,如果要同时处理多个客户端请求,或是在客户端要同时和多个服务器进行通讯,就必须使用多线程来处理。也就是说,将每一个客户端请求分配给一个线程来单独处理。这样做虽然可以达到我们的要求,但同时又会带来另外一个问题。由于每创建一个线程,就要为这个线程分配一定的内存空间(也叫工作存储器),而且操作系统本身也对线程的总数有一定的限制。如果客户端的请求过多,服务端程序可能会因为不堪重负而拒绝客户端的请求,甚至服务器可能会因此而瘫痪。
原文链接:https://blog.youkuaiyun.com/weixin_30794381/article/details/114153319
OS层面:用户进程发起一个IO操作以后边可返回做其它事情,但是用户进程需要时不时的询问IO操作是否就绪,这就要求用户进程不停的去询问,从而引入不必要的CPU资源浪费。其中目前JAVA的NIO就属于同步非阻塞IO。
简介:
- 英文简称NIO
- New lO也有人称之为java non-blocking
- 支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作
- 数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道(先将数据写入到缓冲区,然后再由缓冲区写入通道)
- 目的:解决BIO的大并发问题
- 相关类都被放在java.nio包及子包下,并且对原java.io包中的很多类进行改写。
- 三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器)
- 一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
- JDK1.4之后推出的
- 你调用我,我等待你给我返回结果,我发现此时没有可读写的资源,我不等待先直接返回,等我发现可读写的资源处理以后给你返回结果,此时调用者你不在等待阻塞
- Netty基于NIO模型
- 基于事件驱动思想,当socket有流可读或可写入socket时,操作系统会相应的通知引用程序进行处理,应用再将流读取到缓冲区或写入操作系统。
- 已经不是一个连接就要对应一个处理线程了,而是有效的请求,对应一个线程,当连接没有数据时,是没有工作线程来处理的。
- NIO则是使用单线程或者只使用少量的多线程,每个连接共用一个线程。
- 创建一个连接之后,不再需要对应一个线程,这个连接会被注册到多路复用器上面,所以所有的连接只需要一个线程,当这个连接被多路复用器进行轮询时,发现连接上有请求时,才会开启一个线程进行处理(一个请求一个线程)
- 当一个请求来的时候,开启线程进行处理,可能会等待后端应用的资源(JDBC连接等),此时这个线程就被阻塞了,从并发角度来看,还是会有BIO一样的问题。
-
- HTTP/1.1出现后,有了Http长连接,这样除了超时和指明特定关闭的http header外,这个连接是一直打开的状态的,这样在NIO处理中可以进一步的进化,
- 后端资源中可以实现资源池或者队列,
- 当请求来的话,开启的线程把请求和请求数据传送给后端资源池或者队列里面就返回,并且在全局的地方保持住这个现场(哪个连接的哪个请求等),
- 此时前面的线程还是可以去接受其他的请求,而后端的应用的处理只需要执行队列里面的就可以了,这样请求处理和后端应用是异步的.
- 当后端处理完,到全局地方得到现场,产生响应,这个就实现了异步处理。
-
- 请求绑定channel,channel注册到selector中,selector轮询所有的channel,当channel有准备好的io操作时,selector创建一个线程和buffer进行数据的读写。
- 读数据:channel -----> buffer
- 写数据:buffer --------> channel
- selector:是基于数组实现的,有大小限制,32位机器默认是1024,64位机器默认是2048
- poll:是基于链表实现的,没有大小限制。
- epoll:没有大小限制;不再扫描所有的channel,只扫描活跃的channel;利用mmap()实现内存拷贝。
- Buffer(缓冲区)
- 缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。相比较直接对数组的操作,Buffer APl更加容易操作和管理。
- Channel(通道)
- Java NIO的通道类似流,但又有些不同:既可以从通道中读取数据,又可以写数据到通道。
- 流的(input或output)读写通常是单向的。通道可以非阻塞读取和写入通道,通道可以支持读取或写入缓冲区,也支持异步地读写。
- Selector(选择器)
- Selector是一个Java NIO组件,可以能够检查一个或多个NIO通道,并确定哪些通道已经准备好进行读取或写入。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接,提高效率
- 每个channel对应一个buffer
- 一个线程对应selector,一个selector对应多个channel(连接)程序
- 切换channel决定于事件,selector会根据不同的事件在各个通道上切换
- Buffer是一个内存块,底层是一个数组,由它完成数据的读写,BlO中要么是输入流,或者是输出流,不能双向,但是NIO的Buffer是可以读也可以写
- 核心在于:通道(Channel)和缓冲区(Buffer)。通道表示打开到lO设备(例如:文件、套接字)的连接。若需要使用NIO系统,需要获取用于连接IO设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。
- 简而言之,Channel负责传输,Buffer负责存取数据
- Selector是一个Java NIO组件,可以能够检查一个或多个NIO通道,并确定哪些通道已经准备好进行读取或写入。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接,提高效率
参考 https://blog.youkuaiyun.com/K_520_W/article/details/123454627
三、异步阻塞与异步非阻塞
3.1、异步阻塞:
OS层面:用户进程发起IO操作后,不等待内核IO操作的完成,等内核完成之后会通知APP,这就是同步和异步最关键的区别,同步必须等待或者主动的去询问IO是否完成,那么为什么说是阻塞的呢?它是通过select系统调用来完成,select函数本身的实现方式是阻塞的,select函数有个好处它可以同时监听多个文件句柄,从而提高系统并发性。
3.2、异步非阻塞:
OS层面:用户进程发起一个IO操作后立即返回,等待IO操作真正完成之后,APP会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO读写(真正的IO读写内核已经完成)
简介:
- 英文简称AIO(NIO.2)
- 你调用我,你不等待然后做别的事情,我发现没有可以读写的资源,我也不等待继续做我自己的事,等有可以读写的资源的时候我处理完给你返回结果。
- 异步模型,整个过程,用户线程发起一个系统调用之后无须等待,可以处理别的事情,由操作系统等待接收内容,接收到把数据拷贝到用户进程中,最后通知用户程序已经可以使用数据了, 两个阶段都是非阻塞的。
- 一个有效请求一个线程。
- 进行读写操作时,只须直接调用API的read或write方法即可。这两种方法均为异步的,
- 读操作:当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;
- 写操作:当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。
- 可以理解为,read/write方法都是异步的,会返回一个带回调函数的对象,当执行完读取/写入操作后,直接调用回调函数。
- 在JDK1.7中,这部分内容被称作NIO.2,主要在java.nio.channels包下增加了下面四个异步通道:
- AsynchronousSocketChannel
- AsynchronousServerSocketChannel
- AsynchronousFileChannel
- AsynchronousDatagramChanne
- 实现方式:
- 基于”回调”, 我们可以实现CompletionHandler接口, 在调用时把回调函数传递给对应的API即可
- 返回一个Future. 处理完别的事情, 可以通过isDone()可查看是否已经准备好数据, 通过get()方法等待返回数据
四、举例说明:
小刘煮茶
出场人:小刘、水壶(普通水壶,响水壶)
- 同步阻塞:小刘把普通水壶放到火上,立等水开
- 同步非阻塞:小刘把普通水壶放到火上,去看电视,等一会去看一下水烧开了没有
- 异步阻塞:小刘把响水壶放到火上,立等水开。
- 异步非阻塞:小刘把响水壶放到火上,去看电视,等待水壶响然后去拿壶。
总结:
- 同步异步,相对于水壶而言,普通水壶,同步;响水壶,异步。虽然都能干活,但响水壶可以在自己完工之后,提示小刘水开了。这是普通水壶所不能及的。同步只能让调用者去轮询自己(情况2中),造成小刘效率的低下。调用者去轮询自己
- 阻塞非阻塞,仅仅对于小刘而言。立等的小刘,阻塞;看电视的小刘,非阻塞。情况1和情况3中小刘就是阻塞的,媳妇喊他都不知道。虽然3中响水壶是异步的,可对于立等的小刘没有太大的意义。所以一般异步是配合非阻塞使用的,这样才能发挥异步的效用。
- 同步(Synchronous)
- 异步( Asynchronous)
- 阻塞( Blocking )
- 非阻塞( Nonblocking)
五、总结:
I/O属于底层操作,需要操作系统支持,并发也需要操作系统的支持,所以性能方面不同操作系统差异会比较明显。
- 同步、异步针对调用者来说,同步等待。异步返回干其他等结果再说
- 阻塞、非阻塞针对被调用者来说,阻塞被调用者等待(线程被挂起),非阻塞被调用者直接返回等待返回结果
- BIO:同步阻塞,一个连接一个线程,客户端有连接请求,服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善,适用于连接数目小且固定的架构,对服务器资源要求比较高,并发局限于应用中;jdk1.4之前唯一选择,代码简单,程序简单易懂
- NIO:同步非阻塞,一个请求一个线程,客户端发送的连接请求会注册到多路复用器上,多路复用器轮询到连接有io请求时才会开启一个线程处理,适用于连接数目多且连接比较短(轻操作)的架构,如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
- AIO:异步非阻塞,一个有效连接一个请求,客户端的IO操作都是由os先完成之后再通知服务器去启动线程去处理,适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用os参与并发,编程相当复杂,jdk1.7开始支持。
- 高性能的IO设计:两个比较著名的模式
- Reactor:同步I/O操作
- Proactor模式,异步I/O操作