通过张表简单理解一下三者的区别:
名称 |
jdk支持版本 |
特征 |
IO(BIO) |
1.4之前 |
同步阻塞输入输出 |
NIO |
1.4开始 |
同步非阻塞 |
AIO |
1.7开始 |
异步非阻塞 |
上面说了很多关于同步、异步、阻塞和非阻塞的概念,接下来就具体聊一下它们4个的含义,以及组合之后形成的性能分析。
同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。要么成功都成功,失败都失败,两个任务的状态可以保持一致。而异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。我们可以用打电话和发短信来很好的比喻同步与异步操作。
阻塞与非阻塞主要是从 CPU 的消耗上来说的,阻塞就是 CPU 停下来等待一个慢的操作完成 CPU 才接着完成其它的事。非阻塞就是在这个慢的操作在执行时 CPU 去干其它别的事,等这个慢的操作完成时,CPU 再接着完成后续的操作。虽然表面上看非阻塞的方式可以明显的提高 CPU 的利用率,但是也带了另外一种后果就是系统的线程切换增加。增加的 CPU 使用时间能不能补偿系统的切换成本需要好好评估。
IO的全称其实是:Input/Output的缩写。也就是我们常说的BIO。
传统的 IO 大致可以分为4种类型:
java.net 下提供的 Scoket 很多时候人们也把它归为 同步阻塞 IO ,因为网络通讯同样是 IO 行为。
java.io 下的类和接口很多,但大体都是 InputStream、OutputStream、Writer、Reader 的子集,所有掌握这4个类和File的使用,是用好 IO 的关键。
可以参考下阻塞式I/O模式图:
图解:
BIO的同步过程:
Tread类下面有一个State枚举,这个枚举包含了线程的生命周期:
1.NEW(新建状态)
至今尚未启动的线程的状态。
2.RUNNABLE(可运行状态)
可运行线程的线程状态。处于可运行状态的某一线程正在 Java 虚拟机中运行,但它可能正在等待操作系统中的其他资源,比如处理器。
3.BLOCKED(阻塞状态)
受阻塞并且正在等待监视器锁的某一线程的线程状态。处于受阻塞状态的某一线程正在等待监视器锁,以便进入一个同步的块/方法,或者在调用 Object.wait 之后再次进入同步的块/方法。
4.WAITING(等待状态)
某一等待线程的线程状态。某一线程因为调用下列方法之一而处于等待状态:
LockSupport.park
处于等待状态的线程正等待另一个线程,以执行特定操作。 例如,已经在某一对象上调用了 Object.wait() 的线程正等待另一个线程,以便在该对象上调用 Object.notify() 或 Object.notifyAll()。已经调用了 Thread.join() 的线程正在等待指定线程终止。
5.TIMED_WAITING(定时等待状态)
具有指定等待时间的某一等待线程的线程状态。某一线程因为调用以下带有指定正等待时间的方法之一而处于定时等待状态:
6.TERMINATED(死亡状态)
已终止线程的线程状态。线程已经结束执行。
package io;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
/**
* 等待客户端请求过来
* @author Qian
* @Version 1.0
* @Since JDK1.6
* @Company Bangsun
* @Date 2021/2/4 0:28
*/
public class BioServer {
public static void main(String[] args) {
try( ServerSocket serverSocket = new ServerSocket(8888);) {
System.out.println("BioServer已经启动,监听接口:"+serverSocket.getLocalSocketAddress());
//为了处理很多客户端请求
while (true){
Socket clientSocket = serverSocket.accept();
System.out.println("请求来自:"+clientSocket.getRemoteSocketAddress());
//针对每个Socket进行数据交互的操作,看看是否真的阻塞?当前线程只能等待i/o完成 main-Thread
try (Scanner input = new Scanner(clientSocket.getInputStream())){
//不断地和Socket进行数据交互
while (true){
String request = input.nextLine();
if ("quit".equals(request)){
break;
}
System.out.println(String.format("From %s : %s",clientSocket.getRemoteSocketAddress(),request));
String response = "From BioServer Hello " + request + ".\n";
clientSocket.getOutputStream().write(response.getBytes());
}
}
}
}catch (Exception e){
}
}
}
1.启动main方法
2.telnet连接8888端口
3.输入字段,服务是有的反应的
4.再开启一个cmd,看看是否阻塞;再打字是没反应的,证明是阻塞住了
上述证明io是阻塞式的,我们是否可以通过线程池优化呢?
public class BioServerThreadPool {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
RequestHandler requestHandler = new RequestHandler();
try(ServerSocket serverSocket = new ServerSocket(9999);) {
System.out.println("BioServerThreadPool已经启动,监听接口:" + serverSocket.getLocalSocketAddress());
while (true){
Socket clientSocket = serverSocket.accept();
//针对每个客户端分配一个线程
executor.submit(new ClientHandler(clientSocket,requestHandler));
System.out.println("连接来自于:"+clientSocket.getRemoteSocketAddress());
}
}catch (Exception e){
}
}
}
public class ClientHandler implements Runnable{
private final Socket clientSocket;
private final RequestHandler requestHandler;
public ClientHandler(Socket clientSocket,RequestHandler requestHandler){
this.clientSocket=clientSocket;
this.requestHandler=requestHandler;
}
@Override
public void run() {
//针对每个Socket进行数据交互的操作,看看是否真的阻塞?当前线程只能等待i/o完成 main-Thread
try (Scanner input = new Scanner(clientSocket.getInputStream())){
//不断地和Socket进行数据交互
while (true){
String request = input.nextLine();
if ("quit".equals(request)){
break;
}
System.out.println(String.format("From %s : %s",clientSocket.getRemoteSocketAddress(),request));
String response =requestHandler.handle(request);
clientSocket.getOutputStream().write(response.getBytes());
}
}catch (Exception e){
throw new RuntimeException();
}
}
}
public class RequestHandler {
public String handle(String request){
return "来自于BioServerThreadPool Hello"+request +".\n";
}
}
2.2.3 测试结果
1.启动
2. telnet连接8888端口
3.输入字段,服务是有的反应的
4.前三个都不阻塞,直到第四个才阻塞。
上面的线程池虽然能解决阻塞问题,但线程池依然治标不治本。
那么当服务端单线程时,多个客户端连接服务端,服务端可以同时处理吗?
——NIO
java NIO图解:
过程:
NIO模型图:
java中NIO常用的类:
Netty:本质上就是对NIO的封装和优化
NettyHandler:
Netty的应用:dubbo、rocketMq、spring5web flux等
jdk1.7之后的 NIO2:伪异步的IO
NIO产生原因及应用:是因为业务的发展,单一架构——>分布式架构
对高性能IO的要求,RPC、异步通信MQ、Redis、springCloud、ELK等
package io.nio;
import io.RequestHandler;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
/**
* @author Qian
* @Version 1.0
* @Since JDK1.6
* @Company Bangsun
* @Date 2021/2/4 22:19
*/
public class NioServer {
public static void main(String[] args) throws Exception {
//1.创建一个服务端的Channel
ServerSocketChannel socketChannel = ServerSocketChannel.open();
socketChannel.configureBlocking(false);
//serverChannel需要绑定一个端口
socketChannel.bind(new InetSocketAddress(6666));
System.out.println("Nio 启动,监听端口:"+socketChannel.getLocalAddress());
//2.selector,专门用来轮询,判断io的状态
Selector selector = Selector.open();
//将一个channel注册到selector,channel初始状态
socketChannel.register(selector, SelectionKey.OP_ACCEPT);
//ByteBuffer进行数据的临时存储
ByteBuffer buffer = ByteBuffer.allocate(1024);
RequestHandler requestHandler = new RequestHandler();
//对Selector里面的handler进行轮询,判断谁需要进行后续io操作
while (true){
int select = selector.select();
if (select==0){
continue;
}
//selector中有channel
Set selectionKeys = selector.selectedKeys();
Iterator iterator = selectionKeys.iterator();
while (iterator.hasNext()){
//selectionKeys已经保存了channel的各种信息
SelectionKey key = iterator.next();
//加入channel状态是acceptable
if (key.isAcceptable()){
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = channel.accept();
System.out.println("连接来自:"+clientChannel.getRemoteAddress());
clientChannel.configureBlocking(false);
//将该channel改变状态,read、write
clientChannel.register(selector,SelectionKey.OP_READ);
}
if (key.isReadable()){
//数据交互了
SocketChannel channel = (SocketChannel) key.channel();
//将数据读取到buffer
channel.read(buffer);
String request = new String(buffer.array()).trim();
buffer.clear();
//写数据
System.out.println(String.format("From %s : %s",channel.getRemoteAddress(),request));
String response = requestHandler.handle(request);
channel.write(ByteBuffer.wrap(request.getBytes()));
}
iterator.remove();
}
}
}
}
1.启动
2.telnet 6666端口
3.输入字段,服务器有反应。且开启多个进程,不阻塞
jdk nio2 :伪异步io图解: