同步和异步
-
同步和异步是针对应用程序和内核的交互而言
-
同步
- 用户进程触发
IO
操作 - 等待或轮询的去查看
IO
操作是否就绪
- 用户进程触发
-
异步:
- 用户进程触发
IO
操作以后便开始做自己的事情 IO
操作完成时会得到IO
完成的通知
- 用户进程触发
-
-
例如:银行取款
- 同步:亲自持银行卡到银行取钱
- 使用同步
IO
时,Java
自己处理IO
读写
- 使用同步
- 异步:委托三方拿银行卡到银行取钱,然后给你
- 使用异步
IO
时,Java
将IO
读写委托给OS
处理- 需要将数据缓冲区地址和大小传给
OS
(银行卡和密码),OS
需要支持异步IO
操作API
- 需要将数据缓冲区地址和大小传给
- 使用异步
- 同步:亲自持银行卡到银行取钱
阻塞和非阻塞
-
阻塞和非阻塞:针对于进程在访问数据时,根据
IO
操作就绪状态采取的不同方式-
是一种读取或者写入操作方法的实现方式
-
阻塞方式:读取或者写入函数将一直等待
-
非阻塞方式:读取或者写入方法会立即返回一个状态值
-
-
例如:银行取款
- 阻塞:
ATM
排队取款,只能等待- 使用阻塞
IO
时,Java
调用会一直阻塞到读写完成才返回
- 使用阻塞
- 非阻塞:柜台取款,取号然后等广播会通知办理,没到号就不能去,可以不断询问,如果说还没到就不能去
- 使用非阻塞IO时,如果不能读写
Java
调用会马上返回- 当
IO
事件分发器通知可读写时再继续进行读写,不断循环直到读写完成
- 当
- 使用非阻塞IO时,如果不能读写
- 阻塞:
BIO 编程
BIO
-
Blocking IO
: 同步阻塞的编程方式BIO
编程方式通常是在JDK1.4
版本之前常用的编程方式
-
单独的 TCP 网络开发虽然已经实现了网络程序开发
-
服务器只能为一个客户端线程提供服务
-
若有多人连接访问时无法提供服务
-
-
将每一个连接到服务器的客户端都通过线程对象处理
- 即 服务器上启动多个线程,每个线程单独为一个客户端服务
- 同步并阻塞:服务器实现模式为一个连接一个线程
- 客户端有连接请求时服务器端就需要启动一个线程进行处理
-
BIO
方式适用于连接数目比较小且固定的架构- 对服务器资源要求比较高,并发局限于应用中
- JDK1.4以前的唯一选择,但程序直观简单易理解
-
缺点
- 监听客户端连接时(serverSocket.accept())处于阻塞状态,不能处理其他事务
- 需要为每个客户端建立一个线程,虽然用线程池来优化,但并发较大时,线程开销依旧很大
- 当连接的客户端没有发送数据时,服务器端会阻塞在read操作上,等待客户端输入,造成线程资源浪费
实现过程
-
服务端启动一个
ServerSocket
监听网络请求 -
客户端启动
Socket
发起网络请求- 默认情况下
ServerSocket
会建立一个线程处理此请求
- 默认情况下
-
如果服务端没有线程可用,客户端会阻塞等待或遭到拒绝
-
有线程响应,客户端线程会等待请求结束后,再继续执行
- 建立好的连接,在通讯过程中是同步的,在并发处理效率上比较低
/* ~~~ 服务器端代理线程 ~~~ */ class EchoThread implements Runnable{ private Socket client; public EchoThread(Socket client){ this.clien = client; // 被代理对象 } @Override public void run() { try{ Scanner scanner= new Scanner(client.getInputStream()); // 输入流,读取客户端信息 PrintStream out= new PrintStream(client.getOutputStream()); // 输出流,返回客户端信息 boolean flag = true; // 控制程序结束 while(flag){ if(scanner.hasNext()){ String str = scanner.next().trim(); // 获取客户端信息并去除前后空格 //程序结束 if(str.equalsIgnoreCase("byebye")){ // 匹配到输入 byebye 时控制程序结束 out.println("bye~~~~~~~~~~~~~~~"); flag = false; }else{ out.println("ECHO:"+str); // 返回客户端接收到的数据 } } } scanner.close(); // 关闭流 out.close(); client.close(); // 关闭客户端连接 }catch(Exception e){ e.printStackTrace(); } } public class TestDemo{ public static void main(String[] args) throws Exception{ ServerSocket server = new ServerSocket(9999); // 创建服务器对象 System.out.println("等待客户端连接====="); while(true){ // 持续监听并开启单独线程 Socket client = server.accept(); // 等待客户端连接 new Thread(new EchoThread(client)).start(); // 有客户端连接时开启一个线程 } server.close(); } }
NIO 编程
介绍
-
Unblocking IO
(New IO
):同步非阻塞的编程方式NIO
本身是基于事件驱动思想来完成- 主要想解决
BIO
的大并发问题,出现于JDK 1.4之后 - 相对于
BIO
出现了几个核心的组件Selector
:选择器Channle
:通道Buffer
:缓冲区
-
NIO
最重要的:当一个连接创建后,不需要对应一个线程- 连接会被注册到多路复用器上面
- 所以所有的连接只需要一个线程就可以搞定
- 当线程中的多路复用器进行轮询的时候,发现连接上有请求的话,才开启一个线程进行处理
- 一个请求一个线程模式
- 当连接没有数据时,没有工作线程来处理
- 连接会被注册到多路复用器上面
-
NIO
的处理方式:当一个请求来,开启线程进行处理,可能会等待后端应用的资源(JDBC连接等)- 其实线程已经被阻塞,并发量大时还会有
BIO
一样的问题
- 其实线程已经被阻塞,并发量大时还会有
-
适用连接数目多且连接比较短(轻操作)的架构
- 比如聊天服务器
- 并发局限于应用中
- 编程比较复杂,JDK1.4开始支持
-
Reactor
:一种事件处理模式- 用于处理通过一个或多个输入同时交付给服务处理程序的服务请求
- 服务处理程序对传入的请求进行多路分解,并将它们同步分发到关联的请求处理程序
- NIO 实现多路复用的一种模式
- 当
socket
有流可读或可写入socket
时,操作系统会相应的通知引用程序进行处理- 应用再将流读取到缓冲区或写入操作
-
Netty
:基于Java NIO
类库的异步通信框架- 架构特点:异步非阻塞、基于事件驱动、高性能、高可靠性和高可定制性
- 可以很好地替代掉繁琐的、难以使用的
Java NIO
类库,并提供更多可用的功能 - 实际应用
- 高性能 HTTP 服务器
- 高性能 RPC 框架;例如 Dubbo
- Redission(Redis For Java)
- IM即时通信应用
- 大量需要网络通信的框架底层实现
结构
Buffer
本质
-
NIO
是面向缓冲区, 或面向块编程的NIO
的 IO 传输中,数据会先读入到缓冲区- 当需要时再从缓冲区写出
- 减少了直接读写磁盘的次数,提高了IO传输的效率
-
本质上是可以读写数据的内存块
- 即在内存空间中预留了一定的存储空间,用来缓冲输入和输出的数据
- 预留的存储空间就叫缓冲区。
-
NIO
程序中,通道channel
负责数据的传输- 但是输入和输出的数据都必须经过缓冲区
buffer
- 但是输入和输出的数据都必须经过缓冲区
使用
-
缓冲区的相关类都在
java.nio
包下,最顶层是 Buffer 抽象类 -
重要属性
-
mark:标记
-
position:位置,下一个要被读或写的元素的索引,每次读写缓冲区都会改变该值,为下次读写做准备
-
limit:缓冲区的终点,不能对缓冲区中超过极限的位置进行读写操作,且极限是可修改的
-
capacity:容量,即缓冲区的最多可容纳的数据量,该值在创建缓冲区时被设立,且不可修改
-
-
常用方法
-
方法 作用 int capacity()
返回缓冲区容量 int position()
返回缓冲区位置 int position(int newPosition)
设置缓冲区位置 int limit()
返回缓冲区限制 Buffer limit(int newLimit)
设置缓冲区限制 Buffer mark()
在缓冲区位置设置标记 Buffer reset()
将缓冲区位置设置为先前标记位置 Buffer clear()
清除缓冲区,各属性恢复到默认值;底层数组数据未清空 Buffer flip()
反转缓冲区 Buffer rewind()
倒带缓冲区 int remaining()
返回当前位置到限制之间的元素数 boolean remaining
判断当前位置到限制之间是否存在元素 abstract boolean isReadOnly()
缓冲区是否是只读 abstract boolean hasArray()
缓冲区是否有可访问的底层实现数组 abstract Object array()
返回缓冲区的底层实现数组 abstract int arrayOffset
返回缓冲区底层实现数组的第一个缓冲区元素偏移量 abstract booelan isDirect
缓冲区是否是直接缓冲区
-
常用子类
-
最大区别在于底层实现数组的数据类型
-
ByteBuffer
:存储字节数据到缓冲区 -
CharBuffer
:存储字符数据到缓冲区 -
IntBuffer
:存储整型数据到缓冲区 -
ShortBuffer
:存储短整型数据到缓冲区 -
LongBuffer
:存储长整型数据到缓冲区 -
FloatBuffer
:存储浮点型数据到缓冲区 -
DoubleBuffer
:存储双精度浮点型数据到缓冲区
-
ByteBuffer
-
所有子类中,最常用的是
ByteBuffer
-
常用方法
-
方法 作用 ByteBuffer allocateDirect(int capacity)
创建直接缓冲区,指定容量 ByteBuffer allocate(int capacity)
创建缓冲区,指定容量 ByteBuffer wrap(byte[] array)
创建缓冲区,指定底层实现数组,缓冲区容量、限制为数组大小,位置 0 ByteBuffer wrap(byte[] array, int offSet, int length)
创建缓冲区,指定底层实现数组,缓冲区容量为数组大小,缓冲区限制为 offeSet + length,位置 offSet abstract byte get()
获取当前位置 position 数据,获取后 position 自动 + 1 abstract byte get(int index)
获取指定位置 index 的数据 abstract ByteBuffer put(byte b)
当前位置 position 放入数据 b,放入后 position + 1 abstract ByteByffer put(int index, byte b)
指定位置 index 放入数据 b
-
Channel
介绍
-
NIO
程序中服务器端和客户端之间的数据读写不是通过流,而是通过通道 -
类似于流,都是用来读写数据的,但也有区别的
-
通道是双向的,即可以读也可以写
- 流是单向的,只能读或写
-
通道可以实现异步读写数据
-
通道可以从缓冲区读数据,也可以把数据写入缓冲区
-
-
相关类在
java.nio.channel
包下-
Channel
是一个接口,常用的实现类-
FileChannel
:用于文件的数据读写- 真正的实现类为
FileChannelImpl
- 真正的实现类为
-
DatagramChannel
:用于UDP
的数据读写- 真正的实现类为
DatagramChannelImpl
- 真正的实现类为
-
ServerSocketChannel
:用于监听TCP
连接- 每当有客户端连接时都会创建
SocketChannel
,功能类似ServerSocket
- 真正的实现类为
ServerSocketChannelImpl
- 每当有客户端连接时都会创建
-
SocketChannel
: 用于TCP
的数据读写- 功能类似 节点流 + Socket
- 真正的实现类为
SocketChannelImpl
-
-
FileChannel
-
主要用于对本地文件进行IO操作,如文件复制等
-
常用方法
-
方法 作用 int read(ByteBuffer bst)
将通道中数据读入缓冲区 bst int write(ByteBuffer src)
将缓冲区 src 的数据写入通道 long transferTo(long position, long count, WritableByteChannel target)
将通道数据复制到目标通道 target,复制起始位置 position,复制数据量 count long transferFrom(ReadableByteChannel src, long position, long count)
将目标通道 src 的数据复制到此通道,复制起始位置 position,复制数据量 count
-
-
属性
channel
:默认是空的-
通过流中的
getChanel()
方法根据当前文件流的属性生成对应的FileChannel
-
public FileChannel getChannel() { synchronized (this) { if (channel == null) { channel = FileChannelImpl.open(fd, path, false, true, append, this); } return channel; } } }
-
-
Selector
Selector
-
NIO
程序中,用选择器Selector
实现 一个选择器处理多个通道,即一个线程处理多个连接- 把通道注册到
Selector
上,就可通过Selector
来监测通道 - 如果通道有事件发生,便获取事件通道然后针对每个事件进行相应的处理
- 只有在通道(连接)有真正的读/写事件发生时,才会进行读写操作
- 大大减少了系统开销,不必为每个连接创建线程,不用维护过多线程
- 把通道注册到
-
相关类在
java.nio.channels
包和其子包下-
顶层类是
Selector
抽象类 -
常用方法
-
方法 作用 Selector open()
开启选择器 boolean isOpen()
查看选择器是否已经开启 int select()
检查所有注册的通道是否有事件发生,返回发生事件的通道数量;此方法会阻塞,直到注册的通道有事件发生 int select(long timeout)
检查所有注册的通道是否有事件发生,检查时长 timeout ,返回发生事件的通道数量 int selectNow()
检查所有注册的通道是否有事件发生,所有注册通道检查一遍就返回(不阻塞),返回发生事件的通道数量 Set<SelectionKey> keys()
返回所有注册通道对应的 SelectionKey 集合 Set<SelectionKey> delectedKeys()
返回所有发生事件的通道对应的 SelectionKey 集合
-
-
通道注册
-
ServerSocketChannel
和SocketChannel
类都有注册方法:register(Selector sel, int ops)
sel
:要注册到的选择器ops
:该通道监听的操作事件的类型- 通过该方法将
ServerSocketChannel
或SocketChannel
注册到目标选择器中 - 方法会返回
SelectionKey
储存在注册的Selector
的publicKeys
集合属性- 真正实现类:
SelectionKeyImpl
SelectionKey
储存通道的事件类型和该注册的通道对象- 通过
SelectionKey.channel()
方法获取SelectionKey
对应的通道
- 真正实现类:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-neUkupEz-1660831390542)(images/1563713-20201105210144662-934002093.png)]
选择器检查
-
通过选择器的检查方法,如
select()
得知发生事件的通道数量- 数量大于为 0 时,至少有一个通道发生了事件
-
使用
selectedKeys()
方法获取所有发生事件的通道对应的SelectionKey
- 通过
SelectionKey
中的方法来判断对应通道中需处理的事件类型,根据事件做出相应的处理。
- 通过
-
public final boolean isReadable() { // 判断是否是读操作 return (readyOps() & OP_READ) != 0; } public final boolean isWritable() { // 判断是否是写操作 return (readyOps() & OP_WRITE) != 0; } public final boolean isConnectable() { // 判断是否是连接操作 return (readyOps() & OP_CONNECT) != 0; } public final boolean isAcceptable() { // 判断是否是接收操作 return (readyOps() & OP_ACCEPT) != 0; }
AIO编程
-
Asynchronous IO
: 异步非阻塞的编程方式JDK1.7
中被称为NIO.2
-
对
NIO
的进一步增强-
新增的类
AsynchronousChannel
:支持异步通道- 包括服务端
AsynchronousServerSocketChannel
和普通AsynchronousSocketChannel
等实现
- 包括服务端
CompletionHandler
:用户处理器- 定义一个用户处理就绪事件的接口,由用户自己实现,异步io的数据就绪后回调该处理器消费或处理数据
AsynchronousChannelGroup
:用于资源共享的异步通道集合- 处理
IO
事件和分配给CompletionHandler
- 处理
-
主要在
java.nio.channels
包下增加四个异步通道-
AsynchronousServerSocketChannel
- 提供了
open()
静态工厂 bind()
方法:绑定服务端IP地址、端口号accept()
:用于接收用户连接请求
- 提供了
-
AsynchronousSocketChannel
- 提供
open()
静态工厂方法 - 还提供了
read()
和write()
方法
- 提供
-
AsynchronousFileChannel
-
AsynchronousDatagramChannel
-
-
-
与
NIO
不同:进行读写操作时,只须直接调用API
的read
或write
方法- 两种方法均为异步
- 读操作:当有流可读取时,操作系统会将可读的流传入
read
方法的缓冲区,并通知应用程序 - 写操作:当操作系统将
write
方法传递的流写入完毕时,操作系统主动通知应用程序- 即 read / write 方法都是异步的,完成后会主动调用回调函数
- 读操作:当有流可读取时,操作系统会将可读的流传入
- 两种方法均为异步
-
发出事件(
accept
、read
、write
等)后要指定事件处理类(回调函数)AIO
中的事件处理类:CompletionHandler<V,A>
- 定义如下两个方法,分别在异步操作成功和失败时被回调
void completed(V result, A attachment);
void failed(Throwable exc, A attachment);
- 定义如下两个方法,分别在异步操作成功和失败时被回调
-
适用于连接数目多且连接比较长(重操作)的架构
- 比如相册服务器
- 充分调用OS参与并发操作
- 编程比较复杂,JDK7开始支持