从JavaApi的角度一次搞懂五种IO模型
在说IO模型之前,我们得先知道java程序发起read请求时,操作系统到底干了什么?
操作系统层面的IO
在linux上基于安全的考虑,用户态的CPU只能访问用户空间,内核态的CPU可以访问整个进程空间。并且,只有内核态下可以直接访问各种硬件资源,比如磁盘和网卡。
当CPU处于
Ring3
时为用户态,处于Ring0
时为内核态。
当我们的程序发起阻塞read时,因为网络数据是通过网卡来接收的。处于用户态的CPU无法直接读取网卡中的数据,所以会发生系统调用。然后CPU会从用户态升级到内核态。这时,如果数据未就绪,线程就会阻塞。当系统调用返回时,CPU从内核态重新切换为用户态,并且可以从用户空间的buffer中读到数据。
也就是说,当系统调用返回时,要读取的数据已经从网卡拷贝到内核空间再拷贝到用户空间了
Socket Read的过程
- 用户态CPU执行应用程序代码
- 应用程序发起read操作
- 产生系统调用,CPU由用户态切换为内核态
- 如果要读取的数据尚未就绪,那么当前线程阻塞(其实是将进程的task_struct从运行队列中移动到等待队列中,并触发一次CPU调用,这样就让出了CPU,同时线程处于等待状态)
- 当数据就绪后,数据从内核空间拷贝到用户空间。再将之前阻塞的线程的task_struct从等待队列中移除,重新放入就运行队列中。触发CPU调度后,系统调用返回
- 应用程序读取到数据
思考
从上面的逻辑中可以看出,一次read操作要经历两个大步骤,分别对应上面的步骤4和步骤5:
- 等待数据就绪,就是等数据从网卡拷贝到内核空间中
- 将数据从内核空间复制到用户空间,因为应用程序是运行在用户态的,数据不从内核空间拷贝到用户空间的话,应用程序就无法读到数据。
各种IO模型就是在从不同的角度实现上面的步骤。
五大IO模型
首先:
- IO模型是为了解决内存和外部设备速度差异问题的。
- 阻塞与非阻塞:是指应用程序发起IO操作时,是否立即返回。立即返回为非阻塞,不能立即返回为阻塞。
- 同步与异步:说的是数据从内核空间拷贝到用户空间时,采用的是同步还是异步的方式,目前只有异步IO才是异步拷贝,其他的IO模型都是同步拷贝。
接下来我们以计算服务的demo来应用一下各种IO模型
所有的Server都是用
main
函数启动,所有的Client都是@Test
写在测试包下的
信号驱动IO在实际中并不常用,貌似java中也没有现成的api可以用,所以就先略过了
同步阻塞IO
最最传统的IO操作模式,程序员友好度非常高。缺点是每一个请求都需要创建一个新的线程来处理,对性能开销影响比较大,并不适合高并发的场景。但是如果是连接数小且相对简单的服务也可以用这种IO模型。其实没有空转,不浪费CPU也算是BIO的优点了。
图片转自
BIOServer
public class BIOServer {
private static final Logger logger = LoggerFactory.getLogger(BIOServer.class);
public static void main(String[] args) throws IOException {
ThreadPoolExecutor bizThreadPoolExecutor = createBizThreadPool();
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
logger.info("等待新连接");
//这里用while(true)来接收请求,只要业务处理线程池还吃得下,就可以一直接收请求
//由于是使用线程池处理请求的具体内容,所以就算是连接上了,也不一定能立刻处理
Socket accept = serverSocket.accept();
logger.info("获取到连接了{}", accept);
bizThreadPoolExecutor.execute(() -> {
handleSocket(accept);
});
}
}
private static ThreadPoolExecutor createBizThreadPool() {
return new ThreadPoolExecutor(3, 3, 0, TimeUnit.SECONDS, new SynchronousQueue<>(),
new ThreadFactory() {
private final AtomicInteger atomicInteger = new AtomicInteger();
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "BIOServer-" + atomicInteger.getAndIncrement());
}
},
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
logger.info("当前没有可用的线程了,任务被挂起");
try {
executor.getQueue().put(r);
logger.info("任务成功入队");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
private static void handleSocket(Socket socket) {
logger.info("业务线程处理连接,{}", socket);
InputStream inputStream = null;
OutputStream out = null;
try {
StringBuilder builder = new StringBuilder();
inputStream = socket.getInputStream();
out = socket.getOutputStream();
byte[] bytes = new byte[1024];
//同步阻塞读取,如果没有可读的,就会在这里阻塞
//如果是需要有返回值的请求,那这里就不能while(read!=-1),必须一次性读取
//如果while(read!=-1),当客户端的socket不关闭时,服务端会一直阻塞在inputStream.read(bytes)
//所以bytes的大小比较关键
int read = inputStream.read(bytes);
if(read==-1){
return;
}
builder.append(new String(bytes, 0, read));
logger.info("读取到来自客户端的数据[{}]", builder);
Object result;
try {
result = AviatorEvaluator.execute(builder.toString());
logger.info("AviatorEvaluator的计算结果是[{}]", result);
} catch (Exception e) {
result = e.getMessage();
}
out.write(result.toString().getBytes(StandardCharsets.UTF_8));
out.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
如果使用BIOServer
,那一定要附带一个线程池来处理请求,否则第一个请求没处理完,第二个请求就接不进来。
BIOClient
public class BIOClient {
private static final Logger logger = LoggerFactory.getLogger(BIOClient.class);
@Test
public void test1() throws IOException {
testBIOClient("22+33+44+55");
}
private void testBIOClient(String exp) throws IOException {
logger.info("开始建立连接");
long timeMillis = System.currentTimeMillis();
Socket socket =new Socket("localhost",8080);
logger.info("连接建立成功,耗时{}毫秒",System.currentTimeMillis()-timeMillis);
OutputStream outputStream = socket.getOutputStream();
logger.info("传入计算表达式:{}",exp);
outputStream.write(exp.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
InputStream inputStream = socket.getInputStream();
int read;
byte[] bytes = new byte[1024];
StringBuilder builder = new StringBuilder();
while((read=inputStream.read(bytes))!=-1){
builder.append(new String(bytes,0,read));
}
logger.info("服务器返回结果为:{}", builder);
inputStream.close();
outputStream.close();
socket.close();
}
}
在各个版本的client中,BIOClient算是最最舒服的了,代码从上写到下,一气呵成。
运行结果
- Server端
- Client端
同步非阻塞IO
由于是非阻塞的,所以程序中很多地方都需要用到while(true)
。优点:非阻塞,当客户端建立连接之后,如果还没有发送数据,这时服务端不会卡住。缺点:非阻塞,到处都是while(true)
,性能上得不偿失。
图片转自
NIOServer
public class NIOServer {
private static final Logger logger = LoggerFactory.getLogger(NIOServer.class);
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
//设置为非阻塞模式
serverSocketChannel.configureBlocking(false);
ArrayBlockingQueue<SocketChannel> channels = new ArrayBlockingQueue<>(50);
Thread socketWatcher = new Thread(() -> {
logger.info("socket处理线程启动");
while (true) {
SocketChannel socket = null;
try {
socket = channels.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (socket == null) {
continue;
}
boolean finish = handleSocket(socket);
if (!finish) {
try {
channels.put(socket);
logger.info("{}还没有可读的数据,又放回待处理队列了", socket);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
logger.info("{}已处理完毕", socket);
}
}
}, "socketWatcher");
socketWatcher.start();
while (true) {
logger.info("等待新连接");
SocketChannel accept = serverSocketChannel.accept();
if (accept == null) {
//如果当前没有新连接,那就等1秒再看看
logger.info("没有新的连接,等1秒再看看");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
logger.info("获得新连接,{}", accept);
try {
channels.put(accept);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
private static boolean handleSocket(SocketChannel socketChannel) {
logger.info("业务线程处理连接,{}", socketChannel);
boolean finish = false;
try {
//手动设置为非阻塞模式
socketChannel.configureBlocking(false);
} catch (IOException e) {
e.printStackTrace();
}
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
StringBuilder builder = new StringBuilder();
try {
//因为设置了非阻塞模式,所以read这里并不会阻塞,但是如果没有数据会返回0
int read=socketChannel.read(byteBuffer);
//在没有数据时,不进行下一步处理
if (read==0 || read==-1) {
return false;
}
byteBuffer.flip();
builder.append(StandardCharsets.UTF_8.decode(byteBuffer));
byteBuffer.clear();
logger.info("读取到来自客户端的数据[{}]", builder);
Object result;
try {
result = AviatorEvaluator.execute(builder.toString());
logger.info("AviatorEvaluator的计算结果是[{}]", result);
} catch (Exception e) {
result = e.getMessage();
}
byteBuffer.put(result.toString().getBytes(StandardCharsets.UTF_8));
byteBuffer.flip();
socketChannel.write(byteBuffer);
finish = true;
} catch (IOException e) {
e.printStackTrace();
} finally {
if (finish) {
try {
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return finish;
}
}
NIOClient
public class NIOClient {
private static final Logger logger = LoggerFactory.getLogger(NIOClient.class);
@Test
public void test1() throws IOException {
test("22+33+44+55");
}
private void test(String exp) throws IOException {
logger.info("开始建立连接");
long timeMillis = System.currentTimeMillis();
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress(8080));
boolean finishConnect;
do {
finishConnect = socketChannel.finishConnect();
}while (!finishConnect);
logger.info("连接建立成功,耗时{}毫秒",System.currentTimeMillis()-timeMillis);
logger.info("传入计算表达式:{}",exp);
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
byteBuffer.put(exp.getBytes(StandardCharsets.UTF_8));
byteBuffer.flip();
socketChannel.write(byteBuffer);
byteBuffer.clear();
int read;
do {
read = socketChannel.read(byteBuffer);
}while (read==0 || read==-1);
byteBuffer.flip();
StringBuilder result = new StringBuilder();
result.append(StandardCharsets.UTF_8.decode(byteBuffer));
logger.info("服务器返回结果为:{}", result);
byteBuffer.clear();
socketChannel.close();
}
}
运行结果
- Server端
- Client端
同步非阻塞模型如果在应用中实现的话,基本没有实际应用的机会。这个性能太差了。需要在应用中死循环read操作。在前面我们说过read操作涉及系统调用,本身就是一个重型操作,while(true){read()}
只会让CPU风扇疯响。
多路复用IO
NIO2.0版本,通过selector在操作系统层面判断是否有可读的数据,性能得到了极大的提高。相比传统的BIO能更好的支持并发。多路复用IO适合连接数多且连接比较短的业务场景,比如大名鼎鼎的Redis就是使用多路复用IO。唯一的遗憾是代码写起来有点费脑子…
图片转自
NIO2Server
public class NIO2Server {
private static final Logger logger = LoggerFactory.getLogger(NIO2Server.class);
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
serverSocketChannel.bind(new InetSocketAddress(8080));
logger.info("等待新连接");
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
Map<SocketChannel, Object> resultMap = new HashMap<>();
while (true) {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
if (selectionKey.isAcceptable()) {
ServerSocketChannel serverSocket = (ServerSocketChannel) selectionKey.channel();
SocketChannel newSocket = serverSocket.accept();
newSocket.configureBlocking(false);
newSocket.register(selector, SelectionKey.OP_READ);
logger.info("获取到连接了{}", newSocket);
} else if (selectionKey.isReadable()) {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
logger.info("有可读的连接了{}", socketChannel);
StringBuilder builder = new StringBuilder();
int read;
do {
read = socketChannel.read(byteBuffer);
} while (read == 0 || read == -1);
byteBuffer.flip();
builder.append(StandardCharsets.UTF_8.decode(byteBuffer));
byteBuffer.clear();
logger.info("读取到来自客户端的数据[{}]", builder);
Object result;
try {
result = AviatorEvaluator.execute(builder.toString());
logger.info("AviatorEvaluator的计算结果是[{}]", result);
} catch (Exception e) {
result = e.getMessage();
}
resultMap.put(socketChannel, result);
socketChannel.register(selector, SelectionKey.OP_WRITE);
} else if (selectionKey.isWritable()) {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
logger.info("有可写的连接了{}", socketChannel);
Object result = resultMap.remove(socketChannel);
if (result != null) {
byteBuffer.put(result.toString().getBytes(StandardCharsets.UTF_8));
byteBuffer.flip();
socketChannel.write(byteBuffer);
byteBuffer.clear();
}
socketChannel.close();
}
iterator.remove();
}
}
}
}
其实就是比NIO1.0版本多了Selector
,由Selector
负责阻塞,当有感兴趣的事件到来时再放行。
NIO2Client
public class NIO2Client {
private static final Logger logger = LoggerFactory.getLogger(NIO2Client.class);
@Test
public void test1() throws IOException {
test("22+33+44+55");
}
private void test(String exp) throws IOException {
logger.info("开始建立连接");
long timeMillis = System.currentTimeMillis();
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
Selector selector = Selector.open();
socketChannel.register(selector, SelectionKey.OP_CONNECT);
socketChannel.connect(new InetSocketAddress(8080));
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while(true){
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()){
SelectionKey selectionKey = iterator.next();
iterator.remove();
if (selectionKey.isConnectable()) {
logger.info("连接建立成功,耗时{}毫秒",System.currentTimeMillis()-timeMillis);
SocketChannel channel = (SocketChannel) selectionKey.channel();
channel.configureBlocking(false);
channel.finishConnect();
channel.register(selector, SelectionKey.OP_WRITE);
}else if(selectionKey.isWritable()){
logger.info("传入计算表达式:{}",exp);
SocketChannel channel = (SocketChannel) selectionKey.channel();
byteBuffer.put(exp.getBytes(StandardCharsets.UTF_8));
byteBuffer.flip();
channel.write(byteBuffer);
byteBuffer.clear();
channel.register(selector, SelectionKey.OP_READ);
}else if(selectionKey.isReadable()){
SocketChannel channel = (SocketChannel) selectionKey.channel();
int read = channel.read(byteBuffer);
byteBuffer.flip();
StringBuilder builder = new StringBuilder();
builder.append(StandardCharsets.UTF_8.decode(byteBuffer));
logger.info("服务器返回结果为:{}", builder);
channel.close();
return;
}
}
}
}
}
与服务端版本不同的是,客户端的NIO2.0注册的是SelectionKey.OP_CONNECT
事件,而服务端注册的是SelectionKey.OP_ACCEPT
运行结果
- Server端
- Client端
信号驱动IO
所谓信号驱动式I/O(signal-driven I/O),就是预先告知内核,当某个描述符准备发生某件事情的时候,让内核发送一个信号通知应用进程。信号驱动IO在java应用层面没有对应的API。
异步IO
号称是最完美的IO模型,异步非阻塞IO。JDK1.7以后才支持的,与NIO2.0不同的是,AIO不再需要Selector,而是通过回调的方式通知程序。可以让应用程序仅关注数据。一般适用于连接数较多而且连接时间较长的应用。但是代码写起来真是一言难尽…
需要额外注意的是,AIO需要操作系统的支持,linux上目前依旧是使用
epoll
来模拟的AIO,所以linux上的AIO与多路复用IO效率没啥本质区别。目前只有Windows平台真正从操作系统层面支持了AIO。
AIOServer
public class AIOServer {
private static final Logger logger = LoggerFactory.getLogger(AIOServer.class);
public static void main(String[] args) throws IOException {
AIOServer aioServer = new AIOServer();
AsynchronousServerSocketChannel serverSocketChannel = aioServer.init();
aioServer.listen(serverSocketChannel);
try {
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private AsynchronousServerSocketChannel init() throws IOException {
ThreadPoolExecutor bizThreadPool = createBizThreadPool();
AsynchronousChannelGroup channelGroup = AsynchronousChannelGroup.withThreadPool(bizThreadPool);
AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open(channelGroup);
serverSocketChannel.bind(new InetSocketAddress(8080));
return serverSocketChannel;
}
void listen(AsynchronousServerSocketChannel serverSocketChannel) throws IOException {
logger.info("等待新连接");
serverSocketChannel.accept(this, new CompletionHandler<AsynchronousSocketChannel, AIOServer>() {
@Override
public void completed(AsynchronousSocketChannel socketChannel, AIOServer attachment) {
if (socketChannel.isOpen()) {
logger.info("获取到连接了{}", socketChannel);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
socketChannel.read(byteBuffer, socketChannel, new CompletionHandler<Integer, AsynchronousSocketChannel>() {
@Override
public void completed(Integer result, AsynchronousSocketChannel channel) {
byteBuffer.flip();
StringBuilder builder = new StringBuilder();
builder.append(StandardCharsets.UTF_8.decode(byteBuffer));
byteBuffer.clear();
logger.info("读取到来自客户端的数据[{}]", builder);
Object calcResult;
try {
calcResult = AviatorEvaluator.execute(builder.toString());
logger.info("AviatorEvaluator的计算结果是[{}]", calcResult);
} catch (Exception e) {
calcResult = e.getMessage();
}
byteBuffer.put(calcResult.toString().getBytes(StandardCharsets.UTF_8));
byteBuffer.flip();
channel.write(byteBuffer, channel, new CompletionHandler<Integer, AsynchronousSocketChannel>() {
@Override
public void completed(Integer result, AsynchronousSocketChannel attachment) {
logger.info("数据写入完毕{}", attachment);
byteBuffer.clear();
try {
attachment.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, AsynchronousSocketChannel attachment) {
logger.error("write failed",exc);
}
});
}
@Override
public void failed(Throwable exc, AsynchronousSocketChannel attachment) {
logger.error("read failed",exc);
}
});
}
try {
listen(serverSocketChannel);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, AIOServer attachment) {
logger.error("accept failed",exc);
}
});
}
private ThreadPoolExecutor createBizThreadPool() {
return new ThreadPoolExecutor(3, 3, 0, TimeUnit.SECONDS, new SynchronousQueue<>(),
new ThreadFactory() {
private final AtomicInteger atomicInteger = new AtomicInteger();
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "AIOServer-" + atomicInteger.getAndIncrement());
}
},
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
logger.info("当前没有可用的线程了,任务被挂起");
try {
executor.getQueue().put(r);
logger.info("任务成功入队");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
回调地狱…
AIOClient
public class AIOClient {
private static final Logger logger = LoggerFactory.getLogger(AIOClient.class);
@Test
public void test1() throws IOException {
test("22+33+44+55");
}
private void test(String exp) throws IOException {
logger.info("开始建立连接");
CountDownLatch countDownLatch = new CountDownLatch(1);
long timeMillis = System.currentTimeMillis();
AsynchronousSocketChannel asynchronousSocketChannel = AsynchronousSocketChannel.open();
asynchronousSocketChannel.connect(new InetSocketAddress("localhost",8080), asynchronousSocketChannel, new CompletionHandler<Void, AsynchronousSocketChannel>() {
@Override
public void completed(Void result, AsynchronousSocketChannel attachment) {
logger.info("连接建立成功,耗时{}毫秒",System.currentTimeMillis()-timeMillis);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put(exp.getBytes(StandardCharsets.UTF_8));
byteBuffer.flip();
attachment.write(byteBuffer, attachment, new CompletionHandler<Integer, AsynchronousSocketChannel>() {
@Override
public void completed(Integer result, AsynchronousSocketChannel attachment) {
logger.info("传入计算表达式:{}",exp);
byteBuffer.clear();
attachment.read(byteBuffer, attachment, new CompletionHandler<Integer, AsynchronousSocketChannel>() {
@Override
public void completed(Integer result, AsynchronousSocketChannel attachment) {
StringBuilder builder = new StringBuilder();
byteBuffer.flip();
builder.append(StandardCharsets.UTF_8.decode(byteBuffer));
countDownLatch.countDown();
logger.info("服务器返回结果为:{}", builder);
try {
attachment.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, AsynchronousSocketChannel attachment) {
logger.error("数据读取失败",exc);
}
});
}
@Override
public void failed(Throwable exc, AsynchronousSocketChannel attachment) {
logger.error("数据写入失败",exc);
}
});
}
@Override
public void failed(Throwable exc, AsynchronousSocketChannel attachment) {
logger.error("连接失败",exc);
}
});
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
依旧是回调地狱…
运行结果
- Server端
- Client端
虽然代码写起来有点别扭,但是从日志里可以看到AIO充分利用了多线程的优势,如果linux上更好的支持了AIO,那AIO绝对会成为应用服务IO模型的首选。
后记
- 从我个人的角度看,IO模型的选型更大的意义是存在于服务端的IO模型选型。客户端直接BIO的写法写就完事儿了,易读易维护。但是上面的代码依旧提供了每种IO模型对应的Client的写法,更多的是为了领会不同IO模型的精神而已。
- IO模型给我们提供的应该不仅仅是IO模型,因为大家都知道现在用NIO就是标准答案了,具体操作上使用netty也就就足够了。但是IO模型本质上是为解决两个操作速度不一致的问题的,在应用程序上也会存在两个或多个操作间效率不同的情况,当我们遇到这种情况时,可以考虑IO模型为我们提供的五种解决思路,我觉得这个才是IO模型能给我们带来的好处。
- demo中用到的计算引擎是
aviator
,具体的依赖在下面:implementation 'com.googlecode.aviator:aviator:5.3.0'