【记录】Java NIO实现网络模块遇到的BUG
1.背景
通过JavaNio实现一个简单的网络模块,有点像Netty的线程模型,一个线程(AcceptThread)建立新连接,把新连接绑定到某个SelectorThread,SelectorThread处理读/写。
- AcceptThread:拥有一个Selector,上面只注册了一个ServerSocketChannel,监听客户端新连接,当接收到新连接时,把新连接注册到SelectorThread上。
- SelectorThread:拥有一个Selector,用于注册客户端连接,并且处理读/写事件。
2.代码
public class NonblockingServer {
/** 处理客户端读写 */
private SelectorThread[] selectorThreads;
/** 建立新连接 */
private AcceptThread acceptThread;
/** 选择器 */
private SelectNextSelectorThread nextSelectorThread;
/** 默认selectorThread数量 */
private static int defaultSelectorNum = 3;
public NonblockingServer() {
this(defaultSelectorNum);
}
public NonblockingServer(int selectorThreadNum) {
if (selectorThreadNum < 1) {
throw new IllegalArgumentException("SelectorThread线程数量不能低于1");
}
selectorThreads = new SelectorThread[selectorThreadNum];
try {
for (int i = 0; i < selectorThreads.length; i++) {
selectorThreads[i] = new SelectorThread("selector-thread-" + i);
}
acceptThread = new AcceptThread("accept-thread");
nextSelectorThread = new SelectNextSelectorThread(Arrays.asList(selectorThreads));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public void listen(String host, int port) {
try {
acceptThread.listen(host, port);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 处理连接事件,把新连接注册到一个SelectorThread上
*/
class AcceptThread extends Thread {
private Selector selector;
private ServerSocketChannel serverChannel;
AcceptThread(String name) throws IOException {
super(name);
selector = Selector.open();
}
public void listen(String host, int port) throws IOException {
serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
serverChannel.bind(new InetSocketAddress(host, port));
this.start();
}
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
int select = selector.select();
if (select == 0) {
continue;
}
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
handAccept();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
private void handAccept() throws IOException {
SocketChannel clientChannel = serverChannel.accept();
SelectorThread selectorThread = nextSelectorThread.nextThread();
// 懒加载
if (!selectorThread.isStart()) {
selectorThread.start();
}
selectorThread.register(clientChannel);
}
}
/**
* 处理客户端连接的读/写事件
*/
class SelectorThread extends Thread {
private Selector selector;
private volatile boolean start = false;
SelectorThread(String name) throws IOException {
super(name);
selector = Selector.open();
}
@Override
public void run() {
while (start) {
try {
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (start && iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
SocketChannel channel = (SocketChannel) key.channel();
if (key.isReadable()) {
handRead(channel);
} else if (key.isWritable()) {
handWrite(channel);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 这里简单的把输出打印出来
*/
private void handRead(SocketChannel channel) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
int read = channel.read(buffer);
if (read == -1) {
System.out.println(channel.getRemoteAddress() + " 断开连接");
channel.close();
} else {
byte[] bytes = new byte[read];
buffer.flip();
buffer.get(bytes);
System.out.println(new String(bytes));
buffer.clear();
}
}
private void handWrite(SocketChannel channel) {
// nothing
}
/**
* 把新连接注册到本线程
*/
public void register(SocketChannel clientChannel) throws IOException {
clientChannel.configureBlocking(false);
clientChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
}
@Override
public synchronized void start() {
start = true;
super.start();
}
public void shutdown() {
this.start = false;
this.interrupt();
}
public boolean isStart() {
return start;
}
}
static class SelectNextSelectorThread {
private final Collection<? extends SelectorThread> threads;
private Iterator<? extends SelectorThread> iterator;
public <T extends SelectorThread> SelectNextSelectorThread(Collection<T> threads) {
this.threads = threads;
iterator = this.threads.iterator();
}
/**
* 选择下一个SelectorThread,这里为轮询
*
* @return SelectorThread
*/
public SelectorThread nextThread() {
if (!iterator.hasNext()) {
iterator = threads.iterator();
}
return iterator.next();
}
}
}
NonblockingServer
3.问题
当用客户端连接几次后,发现AcceptThread被卡死了!新连接无法建立。
用jvisualVM可以看到AcceptThread状态为监视,说明此时的AcceptThread在等待某个锁。

通过IDEA的dump threads也可以看到:

观察SelectThread线程:

SelectorImpl.lockAndDoSelect代码:
private int lockAndDoSelect(long var1) throws IOException {
synchronized(this) {
if (!this.isOpen()) {
throw new ClosedSelectorException();
} else {
int var10000;
synchronized(this.publicKeys) {
synchronized(this.publicSelectedKeys) {
var10000 = this.doSelect(var1);
}
}
return var10000;
}
}
}
SelectorImpl.register代码:
protected final SelectionKey register(AbstractSelectableChannel var1, int var2, Object var3) {
if (!(var1 instanceof SelChImpl)) {
throw new IllegalSelectorException();
} else {
SelectionKeyImpl var4 = new SelectionKeyImpl((SelChImpl)var1, this);
var4.attach(var3);
synchronized(this.publicKeys) {
this.implRegister(var4);
}
var4.interestOps(var2);
return var4;
}
}
这两个方法都需要获取this.publicKeys的锁,经过调试发现,SeletorThread在获取Selector的产生的事件时selector.select(),一直在不断的循环执行SelectorImpl.lockAndDoSelect方法,一直在不停的获取this.publicKeys的锁,所以这里的register方法很难有机会拿到this.publicKeys的锁,AcceptThread就卡死在获取锁的过程中了。
4.解决方法
Netty在注册新连接时,是把这个注册过程封装成一个任务,交给SelectorThread执行的,就不会发生线程冲突。
Thrift是把新连接放到SelecttorThread中的BlockingQueue中,也是由SelectorThread执行的。
5.修正后的代码
public class NonblockingServer {
/** 处理客户端读写 */
private SelectorThread[] selectorThreads;
/** 建立新连接 */
private AcceptThread acceptThread;
/** 选择器 */
private SelectNextSelectorThread nextSelectorThread;
/** 默认selectorThread数量 */
private static int defaultSelectorNum = 3;
public NonblockingServer() {
this(defaultSelectorNum);
}
public NonblockingServer(int selectorThreadNum) {
if (selectorThreadNum < 1) {
throw new IllegalArgumentException("SelectorThread线程数量不能低于1");
}
selectorThreads = new SelectorThread[selectorThreadNum];
try {
for (int i = 0; i < selectorThreads.length; i++) {
selectorThreads[i] = new SelectorThread("selector-thread-" + i);
}
acceptThread = new AcceptThread("accept-thread");
nextSelectorThread = new SelectNextSelectorThread(Arrays.asList(selectorThreads));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public void listen(String host, int port) {
try {
acceptThread.listen(host, port);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 处理连接事件,把新连接注册到一个SelectorThread上
*/
class AcceptThread extends Thread {
private Selector selector;
private ServerSocketChannel serverChannel;
AcceptThread(String name) throws IOException {
super(name);
selector = Selector.open();
}
public void listen(String host, int port) throws IOException {
serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
serverChannel.bind(new InetSocketAddress(host, port));
this.start();
}
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
int select = selector.select();
if (select == 0) {
continue;
}
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
handAccept();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
private void handAccept() throws IOException {
SocketChannel clientChannel = serverChannel.accept();
SelectorThread selectorThread = nextSelectorThread.nextThread();
// 懒加载
if (!selectorThread.isStart()) {
selectorThread.start();
}
selectorThread.register(clientChannel);
}
}
/**
* 处理客户端连接的读/写事件
*/
class SelectorThread extends Thread {
private Selector selector;
private BlockingQueue<Runnable> tasks;
private volatile boolean start = false;
SelectorThread(String name) throws IOException {
super(name);
selector = Selector.open();
tasks = new LinkedBlockingQueue<>();
}
@Override
public void run() {
while (start) {
try {
selector.select();
handTask();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (start && iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
SocketChannel channel = (SocketChannel) key.channel();
if (key.isReadable()) {
handRead(channel);
} else if (key.isWritable()) {
handWrite(channel);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
private void handTask() {
Runnable task;
while (start && (task = tasks.poll()) != null) {
task.run();
}
}
/**
* 这里简单的把输出打印出来
*/
private void handRead(SocketChannel channel) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
int read = channel.read(buffer);
if (read == -1) {
System.out.println(channel.getRemoteAddress() + " 断开连接");
channel.close();
} else {
byte[] bytes = new byte[read];
buffer.flip();
buffer.get(bytes);
System.out.println(new String(bytes));
buffer.clear();
}
}
private void handWrite(SocketChannel channel) {
// nothing
}
/**
* 把新连接注册到本线程
*/
public void register(SocketChannel clientChannel) {
submit(() -> {
try {
clientChannel.configureBlocking(false);
clientChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
} catch (Exception e) {
try {
clientChannel.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
});
selector.wakeup();
}
public void submit(Runnable task) {
tasks.offer(task);
}
@Override
public synchronized void start() {
start = true;
super.start();
}
public void shutdown() {
this.start = false;
this.interrupt();
}
public boolean isStart() {
return start;
}
}
static class SelectNextSelectorThread {
private final Collection<? extends SelectorThread> threads;
private Iterator<? extends SelectorThread> iterator;
public <T extends SelectorThread> SelectNextSelectorThread(Collection<T> threads) {
this.threads = threads;
iterator = this.threads.iterator();
}
/**
* 选择下一个SelectorThread,这里为轮询
*
* @return SelectorThread
*/
public SelectorThread nextThread() {
if (!iterator.hasNext()) {
iterator = threads.iterator();
}
return iterator.next();
}
}
}
NonblockingServer
【记录】Java NIO实现网络模块遇到的BUG的更多相关文章
- JAVA NIO学习记录1-buffer和channel
什么是NIO? Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API.NIO与原来的IO有同样的作用和目的,但是使用的方式完全不 ...
- Java NIO学习与记录(八): Reactor两种多线程模型的实现
Reactor两种多线程模型的实现 注:本篇文章例子基于上一篇进行:Java NIO学习与记录(七): Reactor单线程模型的实现 紧接着上篇Reactor单线程模型的例子来,假设Handler的 ...
- java nio的一个严重BUG
java nio的一个严重BUG Posted on 2009-09-28 19:27 dennis 阅读(4588) 评论(5) 编辑 收藏 所属分类: java .源码解读 这个BU ...
- Java nio 空轮询bug到底是什么
编者注:Java nio 空轮询bug也就是Java nio在Linux系统下的epoll空轮询问题. epoll机制是Linux下一种高效的IO复用方式,相较于select和poll机制来说.其高效 ...
- JAVA NIO学习记录2-非阻塞式网络通信
一.阻塞与非阻塞 传统的IO 流都是阻塞式的.也就是说,当一个线程调用read() 或write() 时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务.因此,在完成网络通信 ...
- Java NIO学习与记录(五): 操作系统的I/O模型
操作系统的I/O模型 在开始介绍NIO Reactor模式之前,先来介绍下操作系统的五种I/O模型,了解了这些模型,对理解java nio会有不小的帮助. 先来看下一个服务端处理一次网络请求的流程图: ...
- Java NIO 入门
本文主要记录 Java 中 NIO 相关的基础知识点,以及基本的使用方式. 一.回顾传统的 I/O 刚接触 Java 中的 I/O 时,使用的传统的 BIO 的 API.由于 BIO 设计的类实在太 ...
- 源码分析netty服务器创建过程vs java nio服务器创建
1.Java NIO服务端创建 首先,我们通过一个时序图来看下如何创建一个NIO服务端并启动监听,接收多个客户端的连接,进行消息的异步读写. 示例代码(参考文献[2]): import java.io ...
- java nio系列文章
java nio系列教程 基于NIO的Client/Server程序实践 (推荐) java nio与并发编程相关电子书籍 (访问密码 48dd) 理解NIO nio学习记录 图解ByteBuff ...
随机推荐
- Xcode7.1环境下上架iOS App到AppStore 流程①
前言部分 之前App要上架遇到些问题到网上搜上架教程发现都是一些老的版本的教程 ,目前iTunesConnect 都已经迭代好几个版本了和之前的 界面风格还是有很大的差别的,后面自己折腾了好久才终于把 ...
- gensim Load embeddings
gensim package from gensim.models.keyedvectors import KeyedVectors twitter_embedding_path = 'twitter ...
- C++ 递推法 斐波那契数列 兔子产仔
#include "stdio.h" #include "iostream" int Fibonacci(int n) { int t1, t2; || n = ...
- auto_now_add与auto_now的区别
- Redis原理及拓展
Redis是单线程程序.单线程的Redis为何还能这么快? 1.所有的数据都在内存中,所有的运算都是内存级别的运算(因此时间复杂度为O(n)的指令要谨慎使用) 2.单线程操作,避免了频繁的上下文切换 ...
- UITextfield 允许和禁止编辑
1.enabled属性 2.resignFirstResponder,设置的时候,如果不起作用,可以延时一会儿,因为键盘升起需要时间. dispatch_after(dispatch_time(DIS ...
- JavaScript中的回调地狱及解决方法
1.回调地狱 在使用JavaScript时,为了实现某些逻辑经常会写出层层嵌套的回调函数,如果嵌套过多,会极大影响代码可读性和逻辑,这种情况也被成为回调地狱.比如说你要把一个函数 A 作为回调函数,但 ...
- 【Linux】服务器间免密登录、免确认机器指纹
1.生成密钥 ssh-keygen -t rsa -C "<填写自己方便识别的注释>" -b 4096 没什么问题就执行三次空格. 三次问题是1.填入生成密钥对的路径 ...
- get the deadlock information from sql server
https://stackoverflow.com/questions/12422986/sql-query-to-get-the-deadlocks-in-sql-server-2008 You c ...
- hdu 5564 Clarke and digits
题目链接 http://acm.hdu.edu.cn/showproblem.php?pid=5564 ------------------------------------------------ ...