Java 并发编程-NIO 简明教程
问题来源
在传统的架构中,对于客户端的每一次请求,服务器都会创建一个新的线程或者利用线程池复用去处理用户的一个请求,然后返回给用户结果,这样做在高并发的情况下会存在非常严重的性能问题:对于用户的每一次请求都创建一个新的线程是需要一定内存的,同时线程之间频繁的上下文切换也是一个很大的开销。
p.s: 本文涉及的完整实例代码都可以在我的GitHub上面下载。
什么是Selector
NIO的核心就是Selector,读懂了Selector就理解了异步机制的实现原理,下面先来简单的介绍一下什么是Selector。现在对于客户端的每一次请求到来时我们不再立即创建一个线程进行处理,相反以epool为例子当一个事件准备就绪之后通过回调机制将描述符加入到阻塞队列中,下面只需要通过遍历阻塞队列对相应的事件进行处理就行了,通过这种回调机制整个过程都不需要对于每一个请求都去创建一个线程去单独处理。上面的解释还是有些抽象,下面我会通过具体的代码实例来解释,在这之前我们先来了解一下NIO中两个基础概念Buffer和Channel。
如果大家对于多路IO复用比如select/epool完全陌生的话,建议先读一下我的这篇Linux下的五种IO模型 :-)
Buffer
以ByteBuffer为例子,我们可以通过ByteBuffer.allocate(n)来分配n个字节的缓冲区,对于缓冲区有四个重要的属性:
- capacity,缓冲区的容量,也就是我们上面指定的n。
- position,当前指针指向的位置。
- mark,前一个位置,这里我们下面再解释。
- 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的话分为四种:
- FileChannel从文件中读写数据。
- DatagramChannel以UDP的形式从网络中读写数据。
- SocketChannel以TCP的形式从网络中读写数据。
- 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); |
我们可以监听四种事件:
- SelectionKey.OP_CONNECT:当客户端的尝试连接到服务器
- SelectionKey.OP_ACCEPT:当服务器接受来自客户端的请求
- SelectionKey.OP_READ:当服务器可以从channel中读取数据
- 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 readingwhile (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 readingwhile (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 简明教程的更多相关文章
- 学习笔记(三)--->《Java 8编程官方参考教程(第9版).pdf》:第十章到十二章学习笔记
回到顶部 注:本文声明事项. 本博文整理者:刘军 本博文出自于: <Java8 编程官方参考教程>一书 声明:1:转载请标注出处.本文不得作为商业活动.若有违本之,则本人不负法律责任.违法 ...
- Java并发编程:Thread类的使用
Java并发编程:Thread类的使用 在前面2篇文章分别讲到了线程和进程的由来.以及如何在Java中怎么创建线程和进程.今天我们来学习一下Thread类,在学习Thread类之前,先介绍与线程相关知 ...
- java并发编程系列
1.多线程的概念与使用:java笔记五:多线程的使用 2.多线程产生的问题,解决的方法, 1.引入线程池的原因:Java并发编程:线程池的使用 2.高并发情况下数据库提交:jdbc事务处理, 理解事务 ...
- java 并发编程
闭锁 一种可以延迟线程的进度直到其到达终止状态.可以用来确保某些活动直到其他活动都完成后才继续执行 例如: 确保某个计算在其需要的所有资源都被初始化了之后才继续执行. 确保某个服务在其他依赖的服务都启 ...
- Java并发编程面试题 Top 50 整理版
本文在 Java线程面试题 Top 50的基础上,对部分答案进行进行了整理和补充,问题答案主要来自<Java编程思想(第四版)>,<Java并发编程实战>和一些优秀的博客,当然 ...
- Java并发编程75道面试题及答案
1.在java中守护线程和本地线程区别? java中的线程分为两种:守护线程(Daemon)和用户线程(User). 任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon( ...
- 【转】Java并发编程:Thread类的使用
一.线程的状态 在正式学习Thread类中的具体方法之前,我们先来了解一下线程有哪些状态,这个将会有助于对Thread类中的方法的理解. 线程从创建到最终的消亡,要经历若干个状态.一般来说,线程包括以 ...
- 学习笔记(一)--->《Java 8编程官方参考教程(第9版).pdf》:第一章到六章学习笔记
注:本文声明事项. 本博文整理者:刘军 本博文出自于: <Java8 编程官方参考教程>一书 声明:1:转载请标注出处.本文不得作为商业活动.违者本人不负法律责任.违法者自负一切法律责任. ...
- Java并发编程73道面试题及答案 —— 面试稳了
今天主要整理一下 Java 并发编程在面试中的常见问题,希望对需要的读者有用. 1.在java中守护线程和本地线程区别? java中的线程分为两种:守护线程(Daemon)和用户线程(User). 任 ...
随机推荐
- homework1-201521410029
姓名:孙浩学号: 201521410029指导教师:高见 实验日期:2018年8月9日 1. 虚拟机安装与调试 安装好xp和kali虚拟机之后,查看这三者(包括主机)的i ...
- Mybatis MapperScannerConfigurer 自动扫描 将Mapper接口生成代理注入到Spring
Mybatis MapperScannerConfigurer 自动扫描 将Mapper接口生成代理注入到Spring 非原创[只为记录],原博文地址:https://www.cnblogs.com/ ...
- 贝叶斯公式52张牌猜黑桃A策略
贝叶斯公式52张牌猜黑桃A策略 考虑有208平行世界,其中有4个世界(1/52)的黑桃A方在第一张牌的位置,余下204个世界中,有4个世界的黑桃A在第2张牌的位置,4个世界在第3张牌的位置..... ...
- (转)pycharm常用快捷键
Alt+Enter 自动添加包Ctrl+t SVN更新Ctrl+k SVN提交Ctrl + / 注释(取消注释)选择的行Ctrl+Shift+F 高级查找Ctrl+Enter 补全Shift + En ...
- hbuilder真机调试时,手机端无法连接电脑测试的处理办法
首先保证手机端与电脑处在同一网段 关闭电脑防火墙 电脑服务端需要配置iis网站,同时在hbuilder的manifest.json 配置与iis网站ip一致的url
- PB开发境界 多个DW进行update
多个DW进行update //菜鸟代码dw_1.Update()dw_2.Update()初级代码IF dw_1.Update() = 1 And dw_2.Update() = 1 THEN ...
- Tmux会话的使用
不想看废话的直接拖到下面看干货部分! 我们管理Linux服务器通常是通过ssh远程连接过去,如果在服务器上执行比较耗时的操作,比如下载安装软件.编译等等,如果需要数个小时来完成这些工作,但是又不得不关 ...
- 背水一战 Windows 10 (108) - 通知(Tile): application tile 基础, secondary tile 基础
[源码下载] 背水一战 Windows 10 (108) - 通知(Tile): application tile 基础, secondary tile 基础 作者:webabcd 介绍背水一战 Wi ...
- 脑残式网络编程入门(三):HTTP协议必知必会的一些知识
本文原作者:“竹千代”,原文由“玉刚说”写作平台提供写作赞助,原文版权归“玉刚说”微信公众号所有,即时通讯网收录时有改动. 1.前言 无论是即时通讯应用还是传统的信息系统,Http协议都是我们最常打交 ...
- Javascript高级编程学习笔记(20)—— 创建对象
由于今天有点事,加上对象原型链的东西有点多,所以今天这篇就讲一个小的知识点吧 也算为明天的对象继承做铺垫 工厂模式 虽然使用对象字面量来创建一个对象十分地便捷,但是这个方法有一个显著的缺点 那就是如果 ...