java IO模型有几种?
BIO 即(blocking)同步阻塞IO、NIO 同步非阻塞(non-blocking)IO、AIO 异步非阻塞IO(jdk7推出,可以说是NIO 2.0版)
举个简单的例子,我想喝热水,在家拿水壶烧水。
一开始我比较笨,拿个水壶装上水放到炉子上等着水烧开(同步),在烧水过程中不做任何其他事情(阻塞);
然后我发现这也太无聊了,还是在等着水烧开(这时候还是同步)的时候干点别的吧,打开手机,玩一把游戏,中途时不时地去看一下水是否烧开了(非阻塞);
想着这样还是效率低下,然后我去买了个带响铃的水壶,只要水一烧开就发出响声通知我(异步),这下省心多了;
下次用这个带响铃的来烧水(异步), 我可以继续等着水开而不做其他事情(阻塞) 或者直接自己一边玩去了,水壶响了再去处理(非阻塞);
同步/异步关注的是消息如何通知,在上面的例子就是两种不同的通知方式:同步通知方式是人一直等消息,异步通知方式是由铃声报警器发送消息来通知人,人无需一直去查看消息。阻塞/非阻塞关注的是等待消息通知时的状态,阻塞的时候人的状态不变一直等着,非阻塞的时候人可以变成其他非等待状态,如玩游戏、看书等。
资源浪费、处理能力低下。
来一段BIO的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;
/**
* socket server 端
*
* @author gaojc
* @date 2019/8/20 21:58
*/
public class BIOServer {
public static int port = 8880;
public static void main(String[] args) {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(port);
System.out.println("启动服务端,监听" + port + "端口");
while (true) {
try {
Socket accept = serverSocket.accept();
InputStream inputStream = accept.getInputStream();
BufferedReader bf = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
String line = null;
while ((line = bf.readLine()) != null) {
System.out.println("服务端收到消息:" + line);
}
accept.close();
} catch (IOException e) {
e.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;
/**
* Socket client 端
* 写1个socket通信,客户端向服务端发送数据,服务端打印出来
*
* @author gaojc
* @date 2019/8/20 22:01
*/
public class BIOClient implements Runnable {
String ip;
int port;
public BIOClient(String ip, int port) {
this.ip = ip;
this.port = port;
}
public static void main(String[] args) {
BIOClient bioClient = new BIOClient("127.0.0.1", 8880);
new Thread(bioClient).start();
System.out.println("启动客户端,连接" + bioClient.port + "端口");
}
@Override
public void run() {
try {
while (true) {
Socket socket = new Socket(ip, port);
Scanner scanner = new Scanner(System.in);
System.out.println("客户端开始输入:");
String nextLine = scanner.nextLine();
socket.getOutputStream().write((nextLine).getBytes("UTF-8"));
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
服务端程序 通过while (true) 循环,阻塞的监听来自客户端的连接,阻塞的接收客户端发来的数据并输出。
为了不让接收客户端发来的数据这里阻塞,把服务端程序改造一下,使用多线程:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
/**
* socket server 端
*
* @author gaojc
* @date 2019/8/20 21:58
*/
public class BIOServer {
public static int port = 8880;
public static void main(String[] args) {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(port);
System.out.println("启动服务端,监听" + port + "端口");
while (true) {
try {
Socket socket = serverSocket.accept();
//为每个连接 的读取数据 开 新的线程
new Thread(new ServerThread(socket)).start();
} catch (IOException e) {
e.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
//把接收客户端数据的地方 搞多线程
static class ServerThread implements Runnable {
Socket socket;
public ServerThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
InputStream inputStream = socket.getInputStream();
BufferedReader bf = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
String line = null;
while ((line = bf.readLine()) != null) {
System.out.println("服务端收到消息:" + line);
}
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
如果多线程并发请求过大,这样创建线程过多,线程栈需要系统内存,可能会造成OOM;
线程处理逻辑的代码还需要创建对象,消耗堆内存;
而且多线程使得CPU频繁切换,严重影响性能。
有没办法改进这些弊端?有,想到了线程池,那么来继续改造一下代码:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* socket server 端
*
* @author gaojc
* @date 2019/8/20 21:58
*/
public class BIOServer {
public static int port = 8880;
public static void main(String[] args) {
//复用线程,大大减少线程数量
int threadNum = 2;
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(threadNum, threadNum,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>());
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(port);
System.out.println("启动服务端,监听" + port + "端口");
while (true) {
try {
Socket socket = serverSocket.accept();
//用线程池 读取每个连接的数据
threadPool.execute(new ServerThread(socket));
} catch (IOException e) {
e.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
}
threadPool.shutdown();
}
//把接收客户端数据的地方 搞多线程
static class ServerThread implements Runnable {
Socket socket;
public ServerThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
InputStream inputStream = socket.getInputStream();
BufferedReader bf = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
String line = null;
while ((line = bf.readLine()) != null) {
System.out.println("服务端收到消息:" + line);
}
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
服务端用线程池 来接收客户端的数据,不用为每次连接都创建线程,减少内存开销,节省线程创建的成本
那么这样还是有问题,阻塞对线程池的影响:
阻塞等待接收客户端的数据时,占用线程,而线程池的线程是有限的,并发量大的时候,会导致没有足够的线程来处理请求,请求的响应时间长,甚至拒绝服务。
那么如果能不阻塞,在没有接收到数据的时候去干别的事情,不就好了?
这时候 就需要用到NIO了,NIO可以以 non - blocking模式工作,可以用极少量线程处理大量IO连接。
NIO的工作原理图:
来改造socket服务端的代码,使用NIO
package com.gaojc.springboot.demo.servicefind.IO;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* NIO socket server 端
*
* @author gaojc
* @date 2019/8/22 21:58
*/
public class NIOSocketServer {
public static int port = 8880;
public static int threadNum = 2;
public static void main(String[] args) {
ByteBuffer readBuf = ByteBuffer.allocateDirect(1024);
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(threadNum, threadNum,
10L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(100));
//NIO多路选择器
Selector selector;
ServerSocketChannel serverSocketChannel = null;
//连接计数
int connectionCount = 0;
try {
//1.创建一个选择器
selector = Selector.open();
//2.打开服务器通道
serverSocketChannel = ServerSocketChannel.open();
//3、服务器通道绑定地址
serverSocketChannel.bind(new InetSocketAddress(port));
//4.设置为非阻塞
serverSocketChannel.configureBlocking(false);
//5.把服务器通道注册到selector选择器上,监听阻塞事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("启动服务端,监听" + port + "端口");
//selector循环监听
while (true) {
//1.等待 就绪的事件
int selectCount = selector.select();
//没有就绪的事件就 continue
if (selectCount == 0) {
continue;
}
//2.得到就绪的事件
Set selectionKeys = selector.selectedKeys();
//3.遍历结果
Iterator iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
//如果是阻塞的状态
if (selectionKey.isAcceptable()) {
//获取服务器通道
ServerSocketChannel ssc = (ServerSocketChannel) selectionKey.channel();
SocketChannel sc = null;
try {
//1.调用服务器端的accpet方法获取客户端通道
sc = ssc.accept();
//2.设置为非阻塞
sc.configureBlocking(false);
//3.将客户端通道注册到selector中
sc.register(selector, SelectionKey.OP_READ, ++connectionCount);
} catch (IOException e) {
e.printStackTrace();
}
}
//如果是可读的状态了
if (selectionKey.isReadable()) {
//1.用线程池 读取通道的数据
threadPool.execute(new ServerThread(readBuf, selectionKey));
//2.取消注册,防止线程池处理不及时,重复选择
selectionKey.cancel();
}
//4.处理完了,一定要从迭代里remove
iterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
threadPool.shutdown();
}
//把接收客户端数据的地方 搞多线程
static class ServerThread implements Runnable {
ByteBuffer readBuf;
SelectionKey selectionKey;
public ServerThread(ByteBuffer readBuf, SelectionKey selectionKey) {
this.readBuf = readBuf;
this.selectionKey = selectionKey;
}
@Override
public void run() {
try {
//1、先清空缓冲区,防止有上一次的读数据
this.readBuf.clear();
//2、获取客户端通道
SocketChannel sc = (SocketChannel) selectionKey.channel();
//3、看客户端是否有输入
int count = sc.read(this.readBuf);
if (count == -1) {//如果没有数据
selectionKey.cancel();
sc.close();
}
//4、把buffer转为读模式
this.readBuf.flip();
//5、根据缓冲区的数据长度创建对应大小的byte数组,接收缓冲区的数据
byte[] data = new byte[this.readBuf.remaining()];
//6、将数据从这个缓冲区传输到目标字节数组
this.readBuf.get(data);
//7、将byte数组内容打印出来
String result = new String(data);
System.out.println("接受到client的数据是:" + result);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
这样就使用了非阻塞的IO,使用了selector选择器,做到IO连接多路复用。