Selector多线程
-
单线程可以配合selector,完成对多个Channel的可读写事件的监控。
如果是多核CPU,仅仅使用单线程的话,就无法充分利用多核CPU的优势。
-
另外,只使用单线程,如果某个channel上,处理事件的耗时太长,也会影响其他channel上事件的处理。
selector多线程
一个线程对应一个selector;
一个(线程+selector)处理accept事件,建立连接;
其他(线程+selector)处理其他数据读写事件;
第一版
服务器端主线程:
java
public static void main(String[] args) throws IOException {
Thread.currentThread().setName("Boss");
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(9527));
serverSocketChannel.configureBlocking(false);
Selector selector = Selector.open();
SelectionKey selectionKey = serverSocketChannel.register(selector, 0, null);
selectionKey.interestOps(SelectionKey.OP_ACCEPT);
// 创建固定数量的worker
Worker worker = new Worker("worker-0");
worker.init();
while (true) {
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
// 主线程只负责处理 accept事件
if (key.isAcceptable()) {
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
log.info("客户端已连接:" + socketChannel);
log.info("客户端channel注册到worker线程的 selector-开始");
// 连接建立后,将channel注册到worker线程的 selector上, 并且监听read事件;
// 【这行代码运行在boss线程】
socketChannel.register(worker.selector, SelectionKey.OP_READ, null);
log.info("客户端channel注册到worker线程的 selector-结束");
}
}
}
}
worker线程:
java
// worker线程负责处理 读写事件
static class Worker implements Runnable {
// worker线程,应该有自己的Selector
private Selector selector;
private Thread thread;
private String name;
private volatile boolean start;
public Worker(String name) {
this.name = name;
}
// 初始化线程和selector
public void init() throws IOException {
// 初始化只做一遍
if (!start) {
thread = new Thread(this, name);
selector = Selector.open();
thread.start();
start = true;
}
}
@Override
public void run() {
// worker线程,负责处理 读写事件
while (true) {
try {
// 【该行代码运行在worker线程】
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
if (selectionKey.isReadable()) {
ByteBuffer byteBuffer = ByteBuffer.allocate(16);
SocketChannel channel = (SocketChannel) selectionKey.channel();
channel.read(byteBuffer);
byteBuffer.flip();
CharBuffer decode = Charset.defaultCharset().decode(byteBuffer);
log.info("收到客户端数据:" + decode);
byteBuffer.clear();
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
客户端:
客户端连接到服务器后,立即发送数据;
先触发了服务器连接事件,再出发了read事件。
java
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost",9527));
socketChannel.write(Charset.defaultCharset().encode("1234567890abcdef"));
System.in.read();
}
存在问题:
服务器端打印输出:
text
"C:\Program Files\Java\jdk-1.8\bin\java.exe" ...
Connected to the target VM, address: '127.0.0.1:63211', transport: 'socket'
22:29:39 [INFO ] [Boss] c.w.p.SelectorMultiThread - 客户端已连接:java.nio.channels.SocketChannel[connected local=/127.0.0.1:9527 remote=/127.0.0.1:63544]
22:29:39 [INFO ] [Boss] c.w.p.SelectorMultiThread - 客户端channel注册到worker线程的 selector-开始
服务器端和客户端分别启动后,可以看到客户端成功连接到服务器;
但是客户端channel,没有注册到worker线程的 selector,服务器也没有收到客户端的数据,没有触发read事件;
问题原因:
- 服务器端,创建好worker线程后,就直接初始化了,worker线程启动后,调用了selector的select方法,阻塞住了worker线程;
- 而worker线程的 selector阻塞住之后,就会影响客户端channel的注册;
因此,解决问题的关键,就是【先将客户端channel注册到worker线程的selector上,再调用worker的selector的select方法】。
【客户端channel注册到worker线程的selector上】的逻辑,运行在主线程中;
【调用worker的selector的select方法】的逻辑,运行在worker线程中;
以上两段逻辑的先后顺序如何保证。
思考以下方案:
将worker线程的初始化,和【客户端channel注册到worker线程的selector】,这两个逻辑放在一起,是否可行?
java
log.info("客户端channel注册到worker线程的 selector-开始");
worker.init();
socketChannel.register(worker.selector, SelectionKey.OP_READ, null);
log.info("客户端channel注册到worker线程的 selector-结束");
根据结果来看,似乎可行,但是这种方式,只是碰巧,客户端channel注册到worker线程的selector,然后再调用了select方法,不能真正的保证顺序;
而且,此时如果再来个客户端,此时selector正阻塞在select方法,一定会影响新客户端channel的注册。
text
22:53:47 [INFO ] [Boss] c.w.p.SelectorMultiThread - 客户端已连接:java.nio.channels.SocketChannel[connected local=/127.0.0.1:9527 remote=/127.0.0.1:64486]
22:53:47 [INFO ] [Boss] c.w.p.SelectorMultiThread - 客户端channel注册到worker线程的 selector-开始
22:53:47 [INFO ] [Boss] c.w.p.SelectorMultiThread - 客户端channel注册到worker线程的 selector-结束
22:53:47 [INFO ] [worker-0] c.w.p.SelectorMultiThread - 收到客户端数据:1234567890abcdef
新启动一个客户端,可以看到新客户端也成功连接到服务器,但是没有注册到worker线程的selector上。
text
22:53:47 [INFO ] [Boss] c.w.p.SelectorMultiThread - 客户端已连接:java.nio.channels.SocketChannel[connected local=/127.0.0.1:9527 remote=/127.0.0.1:64486]
22:53:47 [INFO ] [Boss] c.w.p.SelectorMultiThread - 客户端channel注册到worker线程的 selector-开始
22:53:47 [INFO ] [Boss] c.w.p.SelectorMultiThread - 客户端channel注册到worker线程的 selector-结束
22:53:47 [INFO ] [worker-0] c.w.p.SelectorMultiThread - 收到客户端数据:1234567890abcdef
23:06:07 [INFO ] [Boss] c.w.p.SelectorMultiThread - 客户端已连接:java.nio.channels.SocketChannel[connected local=/127.0.0.1:9527 remote=/127.0.0.1:64728]
23:06:07 [INFO ] [Boss] c.w.p.SelectorMultiThread - 客户端channel注册到worker线程的 selector-开始
第二版
在第一版中
【客户端channel注册到worker线程的selector上】的逻辑,运行在主线程中;
【调用worker的selector的select方法】的逻辑,运行在worker线程中;
如果想办法让这两段逻辑运行在同一个线程中,就可以保证执行的先后顺序。
主线程:
java
public static void main(String[] args) throws IOException {
Thread.currentThread().setName("Boss");
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(9527));
serverSocketChannel.configureBlocking(false);
Selector selector = Selector.open();
SelectionKey selectionKey = serverSocketChannel.register(selector, 0, null);
selectionKey.interestOps(SelectionKey.OP_ACCEPT);
// 创建固定数量的worker
Worker worker = new Worker("worker-0");
while (true) {
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
// 主线程只负责处理 accept事件
if (key.isAcceptable()) {
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
log.info("客户端已连接:" + socketChannel);
// 初始化worker
log.info("客户端channel注册到worker线程的 selector-开始");
// 连接建立后,将channel注册到worker线程的 selector上, 并且监听read事件;
worker.init(socketChannel);
log.info("客户端channel注册到worker线程的 selector-结束");
}
}
}
}
worker线程:
java
static class Worker implements Runnable {
private Thread thread;
private Selector selector;
private String name;
private volatile boolean start = false;
private ConcurrentLinkedQueue<Runnable> queue = new ConcurrentLinkedQueue<>();
public Worker(String name) {
this.name = name;
}
// 初始化【线程】和【selector】
public void init(SocketChannel socketChannel) throws IOException {
if (!start) {
this.thread = new Thread(this, name);
selector = Selector.open();
thread.start();
start = true;
}
// 向队列中添加任务,但是任务没有立刻执行
queue.add(() -> {
try {
socketChannel.register(selector, SelectionKey.OP_READ, null);
} catch (ClosedChannelException e) {
throw new RuntimeException(e);
}
});
// 将 selector 从阻塞中唤醒
selector.wakeup();
}
@Override
public void run() {
while (true) {
try {
selector.select();
Runnable runnable = queue.poll();
if (runnable != null) {
runnable.run();
}
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isReadable()) {
ByteBuffer byteBuffer = ByteBuffer.allocate(16);
SocketChannel channel = (SocketChannel) key.channel();
channel.read(byteBuffer);
byteBuffer.flip();
CharBuffer decode = Charset.defaultCharset().decode(byteBuffer);
log.info(String.valueOf(decode));
byteBuffer.clear();
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
启动多个客户端,可以看到正常连接,并且正常接收到数据
text
23:23:52 [INFO ] [Boss] c.w.p.SelectorMultiThread - 客户端已连接:java.nio.channels.SocketChannel[connected local=/127.0.0.1:9527 remote=/127.0.0.1:65125]
23:23:52 [INFO ] [Boss] c.w.p.SelectorMultiThread - 客户端channel注册到worker线程的 selector-开始
23:23:52 [INFO ] [Boss] c.w.p.SelectorMultiThread - 客户端channel注册到worker线程的 selector-结束
23:23:52 [INFO ] [worker-0] c.w.p.SelectorMultiThread - 1234567890abcdef
23:24:03 [INFO ] [Boss] c.w.p.SelectorMultiThread - 客户端已连接:java.nio.channels.SocketChannel[connected local=/127.0.0.1:9527 remote=/127.0.0.1:65137]
23:24:03 [INFO ] [Boss] c.w.p.SelectorMultiThread - 客户端channel注册到worker线程的 selector-开始
23:24:03 [INFO ] [Boss] c.w.p.SelectorMultiThread - 客户端channel注册到worker线程的 selector-结束
23:24:03 [INFO ] [worker-0] c.w.p.SelectorMultiThread - 1234567890abcdef
关键点:
- 使用队列,将【客户端channel注册到selector】的逻辑先保存起来;每次有客户端连接,都要添加这段逻辑;
- 在没有读写事件发生时,worker的selector一定是阻塞的,下次再有客户端连接,一定会注册失败,因此,要通过selector的wakewp方法,唤醒selector;(除了事件可以唤醒selector,也可以手动调用wakeup)
- selector被唤醒后,如果队列中有任务,说明有客户端channel需要注册;
- 客户端channel注册完成后,再处理读写事件;
第三版
服务器端创建,多个 worker,数量为CPU核心数;
Runtime.getRuntime().availableProcessors()
这个方法有坑,如果在Docker环境,拿到的是物理机的核心数,不是容器的核心数。
java
public static void main(String[] args) throws IOException {
Thread.currentThread().setName("Boss");
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(9527));
serverSocketChannel.configureBlocking(false);
Selector selector = Selector.open();
SelectionKey selectionKey = serverSocketChannel.register(selector, 0, null);
selectionKey.interestOps(SelectionKey.OP_ACCEPT);
// 创建固定数量的worker
Worker[] workers = new Worker[Runtime.getRuntime().availableProcessors()];
for (int i = 0; i < workers.length; i++) {
workers[i] = new Worker("worker-" + i);
}
AtomicInteger count = new AtomicInteger();
while (true) {
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
// 主线程只负责处理 accept事件
if (key.isAcceptable()) {
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
log.info("客户端已连接:" + socketChannel);
// 初始化worker
log.info("客户端channel注册到worker线程的 selector-开始");
// 连接建立后,将channel注册到worker线程的 selector上, 并且监听read事件;
workers[count.getAndIncrement() % workers.length].init(socketChannel);
log.info("客户端channel注册到worker线程的 selector-结束");
}
}
}
}
worker:
java
static class Worker implements Runnable {
private Thread thread;
private Selector selector;
private String name;
private volatile boolean start = false;
private ConcurrentLinkedQueue<Runnable> queue = new ConcurrentLinkedQueue<>();
public Worker(String name) {
this.name = name;
}
// 初始化【线程】和【selector】
public void init(SocketChannel socketChannel) throws IOException {
if (!start) {
this.thread = new Thread(this, name);
selector = Selector.open();
thread.start();
start = true;
}
// 向队列中添加任务,但是任务没有立刻执行
queue.add(() -> {
try {
socketChannel.register(selector, SelectionKey.OP_READ, null);
} catch (ClosedChannelException e) {
throw new RuntimeException(e);
}
});
// 将 selector 从阻塞中唤醒
selector.wakeup();
}
@Override
public void run() {
while (true) {
try {
selector.select();
Runnable runnable = queue.poll();
if (runnable != null) {
runnable.run();
}
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isReadable()) {
ByteBuffer byteBuffer = ByteBuffer.allocate(16);
SocketChannel channel = (SocketChannel) key.channel();
channel.read(byteBuffer);
byteBuffer.flip();
CharBuffer decode = Charset.defaultCharset().decode(byteBuffer);
log.info(String.valueOf(decode));
byteBuffer.clear();
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}