问题来源

在传统的架构中,对于客户端的每一次请求,服务器都会创建一个新的线程或者利用线程池复用去处理用户的一个请求,然后返回给用户结果,这样做在高并发的情况下会存在非常严重的性能问题:对于用户的每一次请求都创建一个新的线程是需要一定内存的,同时线程之间频繁的上下文切换也是一个很大的开销。

p.s: 本文涉及的完整实例代码都可以在我的GitHub上面下载。

什么是Selector

NIO的核心就是Selector,读懂了Selector就理解了异步机制的实现原理,下面先来简单的介绍一下什么是Selector。现在对于客户端的每一次请求到来时我们不再立即创建一个线程进行处理,相反以epool为例子当一个事件准备就绪之后通过回调机制将描述符加入到阻塞队列中,下面只需要通过遍历阻塞队列对相应的事件进行处理就行了,通过这种回调机制整个过程都不需要对于每一个请求都去创建一个线程去单独处理。上面的解释还是有些抽象,下面我会通过具体的代码实例来解释,在这之前我们先来了解一下NIO中两个基础概念Buffer和Channel。

如果大家对于多路IO复用比如select/epool完全陌生的话,建议先读一下我的这篇Linux下的五种IO模型 :-)

Buffer

以ByteBuffer为例子,我们可以通过ByteBuffer.allocate(n)来分配n个字节的缓冲区,对于缓冲区有四个重要的属性:

  1. capacity,缓冲区的容量,也就是我们上面指定的n。
  2. position,当前指针指向的位置。
  3. mark,前一个位置,这里我们下面再解释。
  4. limit,最大能读取或者写入的位置。

如上图所示,Buffer实际上也是分为两种,一种用于写数据,一种用于读取数据。

put

通过直接阅读ByteBuffer源码可以清晰看出put方法是把一个byte变量x放到缓冲区中去,同时position加1:

1
2
3
4
5
6
7
8
9
public ByteBuffer put(byte x) {
    hb[ix(nextPutIndex())] = x;
    return this;
}
final int nextPutIndex() {
    if (position >= limit)
        throw new BufferOverflowException();
    return position++;
}

get

get方法是从缓冲区中读取一个字节,同时position加一:

1
2
3
4
5
6
7
8
public byte get() {
    return hb[ix(nextGetIndex())];
}
final int nextGetIndex() {
    if (position >= limit)
        throw new BufferUnderflowException();
    return position++;
}

flip

如果我们想将buffer从写数据的情况变成读数据的情况,可以直接使用flip方法:

1
2
3
4
5
6
public final Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
}

mark和reset

mark是记住当前的位置用的,也就是保存position的值:

1
2
3
4
public final Buffer mark() {
    mark = position;
    return this;
}

如果我们在对缓冲区读写之前就调用了mark方法,那么以后当position位置变化之后,想回到之前的位置可以调用reset会将mark的值重新赋给position:

1
2
3
4
5
6
7
public final Buffer reset() {
    int m = mark;
    if (m < 0)
        throw new InvalidMarkException();
    position = m;
    return this;
}

Channel

利用NIO,当我们读取数据的时候,会先从buffer加载到channel,而写入数据的时候,会先入到channel然后通过channel转移到buffer中去。channel给我们提供了两个方法:通过channel.read(buffer)可以将channel中的数据写入到buffer中,而通过channel.write(buffer)则可以将buffer中的数据写入到到channel中。

Channel的话分为四种:

  1. FileChannel从文件中读写数据。
  2. DatagramChannel以UDP的形式从网络中读写数据。
  3. SocketChannel以TCP的形式从网络中读写数据。
  4. ServerSocketChannel允许你监听TCP连接。

因为今天我们的重点是Selector,所以来看一下SocketChannel的用法。在下面的代码利用SocketChannel模拟了一个简单的server-client程序。

WebServer的代码如下,和传统的sock程序并没有太多的差异,只是我们引入了buffer和channel的概念:

1
2
3
4
5
6
7
8
9
10
11
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress("127.0.0.1", 5000));
SocketChannel socketChannel = ssc.accept();
ByteBuffer readBuffer = ByteBuffer.allocate(128);
socketChannel.read(readBuffer);
readBuffer.flip();
while (readBuffer.hasRemaining()) {
    System.out.println((char)readBuffer.get());
}
socketChannel.close();
ssc.close();

WebClient的代码如下:

1
2
3
4
5
6
7
8
SocketChannel socketChannel = null;
socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1", 5000));
ByteBuffer writeBuffer = ByteBuffer.allocate(128);
writeBuffer.put("hello world".getBytes());
writeBuffer.flip();
socketChannel.write(writeBuffer);
socketChannel.close();

Scatter / Gather

在上面的client程序中,我们也可以同时将多个buffer中的数据放入到一个数组后然后统一放入到channel后传递给服务器:

1
2
3
4
5
6
7
8
ByteBuffer buffer1 = ByteBuffer.allocate(128);
ByteBuffer buffer2 = ByteBuffer.allocate(16);
buffer1.put("hello ".getBytes());
buffer2.put("world".getBytes());
buffer1.flip();
buffer2.flip();
ByteBuffer[] bufferArray = {buffer1, buffer2};
socketChannel.write(bufferArray);

Selector

通过使用selector,我们可以通过一个线程来同时管理多个channel,省去了创建线程以及线程之间进行上下文切换的开销。

创建一个selector

通过调用selector类的静态方法open我们就可以创建一个selector对象:

1
Selector selector = Selector.open();

注册channel

为了保证selector能够监听多个channel,我们需要将channel注册到selector当中:

1
2
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

我们可以监听四种事件:

  1. SelectionKey.OP_CONNECT:当客户端的尝试连接到服务器
  2. SelectionKey.OP_ACCEPT:当服务器接受来自客户端的请求
  3. SelectionKey.OP_READ:当服务器可以从channel中读取数据
  4. SelectionKey.OP_WRITE:当服务器可以向channel中写入数据

对SelectorKey调用channel方法可以得到key对应的channel:

1
Channel channel = key.channel();

而key自身感兴趣的监听事件也可以通过interestOps来获得:

1
int interestSet = selectionKey.interestOps();

对selector调用selectedKeys()方法我们可以得到注册的所有key:

1
Set<SelectionKey> selectedKeys = selector.selectedKeys();

实战

服务器的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress("127.0.0.1", 5000));
ssc.configureBlocking(false);
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
ByteBuffer readBuff = ByteBuffer.allocate(128);
ByteBuffer writeBuff = ByteBuffer.allocate(128);
writeBuff.put("received".getBytes());
writeBuff.flip(); // make buffer ready for reading
while (true) {
    selector.select();
    Set<SelectionKey> keys = selector.selectedKeys();
    Iterator<SelectionKey> it = keys.iterator();
    while (it.hasNext()) {
        SelectionKey key = it.next();
        it.remove();
        if (key.isAcceptable()) {
            SocketChannel socketChannel = ssc.accept();
            socketChannel.configureBlocking(false);
            socketChannel.register(selector, SelectionKey.OP_READ);
        } else if (key.isReadable()) {
            SocketChannel socketChannel = (SocketChannel) key.channel();
            readBuff.clear(); // make buffer ready for writing
            socketChannel.read(readBuff);
            readBuff.flip(); // make buffer ready for reading
            System.out.println(new String(readBuff.array()));
            key.interestOps(SelectionKey.OP_WRITE);
        } else if (key.isWritable()) {
                writeBuff.rewind(); // sets the position back to 0
                SocketChannel socketChannel = (SocketChannel) key.channel();
                socketChannel.write(writeBuff);
                key.interestOps(SelectionKey.OP_READ);
        }
    }
}

客户端程序的代码如下,各位读者可以同时在终端下面多开几个程序来同时模拟多个请求,而对于多个客户端的程序我们的服务器始终只用一个线程来处理多个请求。一个很常见的应用场景就是多个用户同时往服务器上传文件,对于每一个上传请求我们不在单独去创建一个线程去处理,同时利用Executor/Future我们也可以不用阻塞在IO操作中而是立即返回用户结果。

1
2
3
4
5
6
7
8
9
10
11
12
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1", 5000));
ByteBuffer writeBuffer = ByteBuffer.allocate(32);
ByteBuffer readBuffer = ByteBuffer.allocate(32);
writeBuffer.put("hello".getBytes());
writeBuffer.flip(); // make buffer ready for reading
while (true) {
    writeBuffer.rewind(); // sets the position back to 0
    socketChannel.write(writeBuffer); // hello
    readBuffer.clear(); // make buffer ready for writing
    socketChannel.read(readBuffer); // recieved
}

Java 并发编程-NIO 简明教程的更多相关文章

  1. 学习笔记(三)--->《Java 8编程官方参考教程(第9版).pdf》:第十章到十二章学习笔记

    回到顶部 注:本文声明事项. 本博文整理者:刘军 本博文出自于: <Java8 编程官方参考教程>一书 声明:1:转载请标注出处.本文不得作为商业活动.若有违本之,则本人不负法律责任.违法 ...

  2. Java并发编程:Thread类的使用

    Java并发编程:Thread类的使用 在前面2篇文章分别讲到了线程和进程的由来.以及如何在Java中怎么创建线程和进程.今天我们来学习一下Thread类,在学习Thread类之前,先介绍与线程相关知 ...

  3. java并发编程系列

    1.多线程的概念与使用:java笔记五:多线程的使用 2.多线程产生的问题,解决的方法, 1.引入线程池的原因:Java并发编程:线程池的使用 2.高并发情况下数据库提交:jdbc事务处理, 理解事务 ...

  4. java 并发编程

    闭锁 一种可以延迟线程的进度直到其到达终止状态.可以用来确保某些活动直到其他活动都完成后才继续执行 例如: 确保某个计算在其需要的所有资源都被初始化了之后才继续执行. 确保某个服务在其他依赖的服务都启 ...

  5. Java并发编程面试题 Top 50 整理版

    本文在 Java线程面试题 Top 50的基础上,对部分答案进行进行了整理和补充,问题答案主要来自<Java编程思想(第四版)>,<Java并发编程实战>和一些优秀的博客,当然 ...

  6. Java并发编程75道面试题及答案

    1.在java中守护线程和本地线程区别? java中的线程分为两种:守护线程(Daemon)和用户线程(User). 任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon( ...

  7. 【转】Java并发编程:Thread类的使用

    一.线程的状态 在正式学习Thread类中的具体方法之前,我们先来了解一下线程有哪些状态,这个将会有助于对Thread类中的方法的理解. 线程从创建到最终的消亡,要经历若干个状态.一般来说,线程包括以 ...

  8. 学习笔记(一)--->《Java 8编程官方参考教程(第9版).pdf》:第一章到六章学习笔记

    注:本文声明事项. 本博文整理者:刘军 本博文出自于: <Java8 编程官方参考教程>一书 声明:1:转载请标注出处.本文不得作为商业活动.违者本人不负法律责任.违法者自负一切法律责任. ...

  9. Java并发编程73道面试题及答案 —— 面试稳了

    今天主要整理一下 Java 并发编程在面试中的常见问题,希望对需要的读者有用. 1.在java中守护线程和本地线程区别? java中的线程分为两种:守护线程(Daemon)和用户线程(User). 任 ...

随机推荐

  1. 2019.03.28 bzoj3326: [Scoi2013]数数(数位dp)

    传送门 题意: 一个人数数,规则如下: 确定数数的进制B 确定一个数数的区间[L, R] 对于[L, R] 间的每一个数,把该数视为一个字符串,列出该字符串的所有连续子串对应的B进制数的值. 对所有列 ...

  2. WindowsPE权威指南 第二章 小工具 PEInfo代码的C语言实现

    主程序代码 PEInfo.c #include <Windows.h> #include<Richedit.h> #include "resource.h" ...

  3. 15. Life Cycle of the Products 产品的生命周期

    15. Life Cycle of the Products 产品的生命周期 (1) We can see how the product life cycle works by looking at ...

  4. Nikto学习点

    一.使用Nikto扫描https站点 nikto.pl -output myhost.html -g -ssl -Format HTM -host www.test.com -port 443 二.使 ...

  5. Springfox与swagger的整合使用

    一.前言 让我们先理一下springfox与swagger的关系. swagger是一个流行的API开发框架,这个框架以“开放API声明”(OpenAPI Specification,OAS)为基础, ...

  6. python 单例模式的四种实现方法及注意事项

    一.模块单例 Python 的模块就是天然的单例模式,因为模块在第一次导入时,会生成 .pyc 文件,当第二次导入时,就会直接加载 .pyc 文件,而不会再次执行模块代码. #foo1.py clas ...

  7. docker 私有 repository

    为什么需要私有仓库? 首先假如公司内部有两台以上的服务器,使用docker管理应用程序,我在A服务器上创建了一个.net core 的镜像,需要在B服务器上共享这个镜像,怎么办?当然不能拷贝一份代码在 ...

  8. nginx三种安装方法(转载)

    Nginx是一款轻量级的网页服务器.反向代理服务器.相较于Apache.lighttpd具有占有内存少,稳定性高等优势.它最常的用途是提供反向代理服务. 1.安装包编译安装 2.yum源安装 3.使用 ...

  9. @ResponseBody 返回乱码 的解决办法

    1:最快的  最简单的办法是  在Ajax请求脸面指定头信息Accept属性,StringHttpMessageConverter默认iso-8859-1编码,但是会根据请求头信息指定的编码格式来转换 ...

  10. linux 性能优化

    linux的性能优化: 1.CPU,MEM 2.DISK--RAID 3.网络相关的外设,网卡 linux系统性能分析: top:linux系统的负载,CPU,MEM,SWAP,占用CPU和内存比较的 ...