读事件独立线程处理的问题
-
当把读事件放到单独的线程中执行,要注意,读事件的的延迟问题,即如果没有及时处理(将数据从
Channel
中读取处理)又没有将其注销,那么下一次迭代的时候,依然会触发读事件,这可能会产生一些问题public class ReadEvent { private ServerSocketChannel serverSocketChannel; private Selector selector; public ReadEvent() throws IOException { init(); } public void init() throws IOException { serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.socket().setReuseAddress(true); serverSocketChannel.bind(new InetSocketAddress(8888)); System.out.println("启动服务并绑定端口"); } private void handler() throws IOException { SocketChannel channel = serverSocketChannel.accept(); channel.configureBlocking(false); System.out.println("接收一个新连接"); selector = Selector.open(); channel.register(selector, SelectionKey.OP_READ); while (true) { int n = selector.select(); System.out.println(n); if (n == 0) { continue; } Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator(); while (keyIterator.hasNext()) { SelectionKey selectionKey = keyIterator.next(); System.out.println(selectionKey); keyIterator.remove(); if (selectionKey.isReadable()) { System.out.println("处理读事件"); try { /** * 防止起了太多的线程,因为ReadTask执行有2s的延迟,所以此处如果 * 不休眠会导致ReadTask不断的创建,即主要为了防止启动太多的线程 */ Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } /** * 因为是多线程,可能读事件还没处理(将数据从 channel 中读取处理),主线程就结束了 * 那么下一次迭代的时候, 仍然会触发读事件, 这样一个 channel 中的数据就会被多个线程进行处理 */ new ReadTask(selectionKey).start(); } } } } public static void main(String[] args) throws IOException { new ReadEvent().handler(); } } class ReadTask extends Thread { private SelectionKey key; private String name; private Charset charset = Charset.forName("UTF-8"); private static final AtomicInteger COUNTER = new AtomicInteger(1); public ReadTask(SelectionKey key) { this.key = key; this.name = "ReadTask-" + COUNTER.getAndAdd(1); } @Override public void run() { //模拟读延迟 mockReadDelay(2000); SocketChannel channel = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(64); try { int size = channel.read(buffer); buffer.flip(); System.err.println(String.format("%s读取%s bytes,消息内容:%s", name, size, this.decode(buffer))); } catch (IOException e) { e.printStackTrace(); } } public String decode(ByteBuffer buffer) { CharBuffer charBuffer = charset.decode(buffer); return charBuffer.toString(); } public void mockReadDelay(long millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }
-
输出结果如下,可以看出因为读事件线程的延迟处理(从Channel中读取数据)导致读事件被触发了三次,三个不同的线程来处理。
启动服务并绑定端口 接收一个新连接 1 sun.nio.ch.SelectionKeyImpl@4617c264 处理读事件 1 sun.nio.ch.SelectionKeyImpl@4617c264 处理读事件 1 sun.nio.ch.SelectionKeyImpl@4617c264 处理读事件 ReadTask-1读取5 bytes,消息内容:Aaaaa ReadTask-2读取0 bytes,消息内容: ReadTask-3读取0 bytes,消息内容:
-
怎么解决这个问题?通常的做法是服务器收到
OP_READ
事件之后,在下一次进行select()操作之前会将OP_READ
先注销掉,以防止一个连接的读操作被分到多个线程中去.(这里只是实验性质来说明会有这个问题,所以不考虑粘包和拆包)public class ReadEvent { private ServerSocketChannel serverSocketChannel; private Selector selector; public ReadEvent() throws IOException { init(); } public void init() throws IOException { serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.socket().setReuseAddress(true); serverSocketChannel.bind(new InetSocketAddress(8888)); System.out.println("启动服务并绑定端口"); } private void handler() throws IOException { SocketChannel channel = serverSocketChannel.accept(); channel.configureBlocking(false); System.out.println("接收一个新连接"); selector = Selector.open(); channel.register(selector, SelectionKey.OP_READ); while (true) { int n = selector.select(); System.out.println(n); if (n == 0) { continue; } Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator(); while (keyIterator.hasNext()) { SelectionKey selectionKey = keyIterator.next(); System.out.println(selectionKey); keyIterator.remove(); //增加注销操作 int readyOps = selectionKey.readyOps(); //&~xx 代表取消事件 selectionKey.interestOps(selectionKey.interestOps() & ~readyOps); if (selectionKey.isReadable()) { System.out.println("处理读事件"); try { /** * 防止起了太多的线程,因为ReadTask执行有2s的延迟,所以此处如果 * 不休眠会导致ReadTask不断的创建,即主要为了防止启动太多的线程 */ Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } /** * 因为是多线程,可能读事件还没处理(将数据从 channel 中读取处理),主线程就结束了 * 那么下一次迭代的时候, 仍然会触发读事件, 这样一个 channel 中的数据就会被多个线程进行处理 */ new ReadTask(selectionKey).start(); } } } } public static void main(String[] args) throws IOException { new ReadEvent().handler(); } } class ReadTask extends Thread { private SelectionKey key; private String name; private Charset charset = Charset.forName("UTF-8"); private static final AtomicInteger COUNTER = new AtomicInteger(1); public ReadTask(SelectionKey key) { this.key = key; this.name = "ReadTask-" + COUNTER.getAndAdd(1); } @Override public void run() { //模拟读延迟 mockReadDelay(2000); SocketChannel channel = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(64); try { int size = channel.read(buffer); buffer.flip(); System.err.println(String.format("%s读取%s bytes,消息内容:%s", name, size, this.decode(buffer))); } catch (IOException e) { e.printStackTrace(); } } public String decode(ByteBuffer buffer) { CharBuffer charBuffer = charset.decode(buffer); return charBuffer.toString(); } public void mockReadDelay(long millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }
-
输出
启动服务并绑定端口 接收一个新连接 1 sun.nio.ch.SelectionKeyImpl@4617c264 处理读事件 ReadTask-1读取5 bytes,消息内容:Aaaaa
解决方案
tomcat解决方案
- tomcat8.0.30
- 每次都将读取到的数据放到一个接收队列中,并且注销
OP_READ
事件,在处理完请求之后再重新注册OP_READ
,等待后续操作。NioEndpoint.Poller#register()
public void register(final NioChannel socket) {
socket.setPoller(this);
KeyAttachment ka = new KeyAttachment(socket);
ka.setPoller(this);
ka.setTimeout(getSocketProperties().getSoTimeout());
ka.setKeepAliveLeft(NioEndpoint.this.getMaxKeepAliveRequests());
ka.setSecure(isSSLEnabled());
PollerEvent r = eventCache.pop();
ka.interestOps(SelectionKey.OP_READ);//this is what OP_REGISTER turns into.
if ( r==null) r = new PollerEvent(socket,ka,OP_REGISTER);
else r.reset(socket,ka,OP_REGISTER);
//添加到事件队列
addEvent(r);
}