
上一篇我们介绍了worker并且还写好了worker的代码我们启动了服务端代码并且启动客户端发送了消息但是发现报了 “this.workerSelector” is null。这是什么原因呢问题一this.workerSelector is null一、核心原因workerSelector 为 null因为 register() 方法中线程启动与 Selector 初始化的顺序错误。在 register() 方法中你先执行了 thread.start() 启动了 Worker 线程随后才执行 workerSelector Selector.open()。由于线程启动是异步的Worker 线程的 run() 方法可能在主线程执行到赋值语句之前就已经开始运行此时访问 workerSelector 必然为空导致 select() 抛出空指针异常NPE。二、详细时序分析主线程调用 worker.register()进入 if(!start) 块。创建并启动线程thread.start()。此时 JVM 调度 Worker 线程进入就绪状态可能立即执行 run() 方法。竞态条件发生点如果 Worker 线程抢占了 CPU它会执行 workerSelector.select()。此时主线程尚未执行下一行代码workerSelector 仍为默认值 null。主线程继续执行workerSelector Selector.open()。但这对于已经崩溃的 Worker 线程来说太晚了。Worker 线程执行 run()进入 while(true) 循环。执行 workerSelector.select()。由于 workerSelector 是 null直接抛出 NullPointerException。三、修复方案必须保证 Selector 初始化完成后再启动线程或者使用同步机制确保线程启动时 Selector 已就绪。方案 1调整初始化顺序推荐最简单先初始化 Selector再启动线程。publicvoidregister()throwsIOException{if(!start){// 1. 先初始化 SelectorworkerSelectorSelector.open();// 2. 再启动线程确保 run() 执行时 selector 已非空threadnewThread(this,name);thread.start();starttrue;}}此时问题解决方案 2使用 CountDownLatch 同步更严谨如果初始化逻辑复杂可以使用锁或Latch确保线程等待初始化完成。privateCountDownLatchlatchnewCountDownLatch(1);publicvoidregister()throwsIOException{if(!start){threadnewThread(this,name);thread.start();// 主线程初始化 SelectorworkerSelectorSelector.open();starttrue;// 通知 Worker 线程可以开始工作了latch.countDown();}}Overridepublicvoidrun(){try{// 等待主线程初始化完成latch.await();}catch(InterruptedExceptione){Thread.currentThread().interrupt();return;}while(true){try{workerSelector.select();// ... 后续处理逻辑}catch(IOExceptione){thrownewRuntimeException(e);}}}我们在看一个问题修改了服务端代码将 worker.register();的位置换了问题二 workerSelector.select();被阻塞后面的代码没有执行packagecom.example.demo;importlombok.extern.slf4j.Slf4j;importjava.io.IOException;importjava.net.InetSocketAddress;importjava.nio.ByteBuffer;importjava.nio.channels.*;importjava.util.Iterator;importjava.util.Set;Slf4jpublicclassMultiNioServerTest{publicstaticvoidmain(String[]args)throwsIOException{Thread.currentThread().setName(boss);ServerSocketChannelsscServerSocketChannel.open();ssc.configureBlocking(false);SelectorbossSelector.open();SelectionKeybosskeyssc.register(boss,0,null);bosskey.interestOps(SelectionKey.OP_ACCEPT);ssc.bind(newInetSocketAddress(8081));// 1创建固定数据的worker并初始化WorkerworkernewWorker(worker-0);worker.register();while(true){boss.select();IteratorSelectionKeyiteratorboss.selectedKeys().iterator();while(iterator.hasNext()){SelectionKeykeyiterator.next();iterator.remove();if(key.isAcceptable()){SocketChannelscssc.accept();sc.configureBlocking(false);log.info(connected-------{},sc.getRemoteAddress());// 2,关联selectorlog.info(before register-------{},sc.getRemoteAddress());// worker.register();sc.register(worker.workerSelector,SelectionKey.OP_READ,null);log.info(after register-------{},sc.getRemoteAddress());// 【核心修复】唤醒 Worker 线程使其从 select() 中返回// worker.workerSelector.wakeup();}}}}staticclassWorkerimplementsRunnable{privateThreadthread;publicSelectorworkerSelector;privateStringname;privatevolatilebooleanstartfalse;publicWorker(Stringname){this.namename;}// 初始化线程和selectorpublicvoidregister()throwsIOException{if(!start){workerSelectorSelector.open();threadnewThread(this,name);thread.start();starttrue;}}Overridepublicvoidrun(){while(true){try{workerSelector.select();SetSelectionKeyselectionKeysworkerSelector.selectedKeys();IteratorSelectionKeyiteratorselectionKeys.iterator();while(iterator.hasNext()){SelectionKeykeyiterator.next();iterator.remove();if(key.isReadable()){ByteBufferbufferByteBuffer.allocate(16);SocketChannelchannel(SocketChannel)key.channel();log.info(read-------{},channel.getRemoteAddress());channel.read(buffer);buffer.flip();while(buffer.hasRemaining()){System.out.print((char)buffer.get());}System.out.println();}}}catch(IOExceptione){thrownewRuntimeException(e);}}}}}看下图什么结果也没有输出这是为什么呢一、核心原因workerSelector.select() 阻塞未唤醒因为 Boss 线程注册 Channel 到 Worker Selector 时存在竞态条件或未及时唤醒。在 Reactor 模式中当 Boss 线程将新连接的 SocketChannel 注册到 Worker 的 Selector 上时如果 Worker 线程正阻塞在 select() 方法中必须调用 selector.wakeup() 才能立即返回并处理新注册的事件。若未调用 wakeup()Worker 线程可能会一直阻塞直到超时如果有设置或下一个事件发生导致“下面代码没有执行”的现象。二、详细分析与修复1. 问题复现逻辑Worker 线程启动执行 workerSelector.select()进入阻塞状态。Boss 线程接受连接sc.configureBlocking(false)worker.register() (确保 Worker 已启动)sc.register(worker.workerSelector, SelectionKey.OP_READ, buffer)关键点缺失Boss 线程注册完后没有调用 worker.workerSelector.wakeup()。结果Worker 线程仍然阻塞在 select()不知道有新 Channel 注册进来因此不会执行后续的 isReadable 逻辑。2. 修复方案添加 wakeup()在 Boss 线程中注册完 Channel 后立即唤醒 Worker 的 Selector。// Boss 线程中的代码if(key.isAcceptable()){SocketChannelscssc.accept();sc.configureBlocking(false);log.info(connected-------{},sc.getRemoteAddress());// 确保 Worker 已启动worker.register();// 注册到 Worker Selector并绑定 Buffer (解决 attachment 为 null 的问题)ByteBufferbufferByteBuffer.allocate(1024);sc.register(worker.workerSelector,SelectionKey.OP_READ,buffer);// 【核心修复】唤醒 Worker 线程使其从 select() 中返回worker.workerSelector.wakeup();log.info(after register-------{},sc.getRemoteAddress());}3. 补充修复Worker 线程的健壮性确保 Worker 线程在 select() 返回后能正确处理事件并处理异常防止线程崩溃。staticclassWorkerimplementsRunnable{privateThreadthread;publicSelectorworkerSelector;// 改为 public 以便 Boss 访问 wakeupprivateStringname;privatevolatilebooleanstartfalse;publicWorker(Stringname){this.namename;}publicvoidregister()throwsIOException{if(!start){workerSelectorSelector.open();threadnewThread(this,name);thread.start();starttrue;}}Overridepublicvoidrun(){while(true){try{// 阻塞等待事件workerSelector.select();SetSelectionKeyselectionKeysworkerSelector.selectedKeys();IteratorSelectionKeyiteratorselectionKeys.iterator();while(iterator.hasNext()){SelectionKeykeyiterator.next();iterator.remove();// 必须移除try{if(key.isReadable()){SocketChannelchannel(SocketChannel)key.channel();ByteBufferbuffer(ByteBuffer)key.attachment();if(buffernull){bufferByteBuffer.allocate(1024);key.attach(buffer);}intreadchannel.read(buffer);if(read-1){// 客户端关闭log.info(Client disconnected);key.cancel();channel.close();}elseif(read0){buffer.flip();// 处理数据...log.info(Received: {},Charset.defaultCharset().decode(buffer));buffer.clear();// 或 compact()}}}catch(IOExceptione){log.error(Read error,e);key.cancel();try{channel.close();}catch(IOExceptionex){}}}}catch(IOExceptione){log.error(Select error,e);}}}}三、总结现象Worker 线程卡在 select()不处理新连接。原因Boss 线程注册新 Channel 后未唤醒 Worker 的 Selector。解决在 sc.register(…) 后调用 worker.workerSelector.wakeup()。注意确保 workerSelector 对 Boss 线程可见如设为 public 或通过 getter 访问且注册时绑定了非空的 Attachment (ByteBuffer)。换了位置导致执行是晚了 workerSelector.select(); 被阻塞了没有被唤醒