查看原文
其他

阿里面试题BIO和NIO数量问题附答案和代码

王磊的博客 Java中文社群 2021-06-30

一、问题

BIO 和 NIO 作为 Server 端,当建立了 10 个连接时,分别产生多少个线程?

答案: 因为传统的 IO 也就是 BIO 是同步线程堵塞的,所以每个连接都要分配一个专用线程来处理请求,这样 10 个连接就会创建 10 个线程去处理。而 NIO 是一种同步非阻塞的 I/O 模型,它的核心技术是多路复用,可以使用一个链接上的不同通道来处理不同的请求,所以即使有 10 个连接,对于 NIO 来说,开启 1 个线程就够了。

二、BIO 代码实现

  1. public class DemoServer extends Thread {

  2. private ServerSocket serverSocket;

  3. public int getPort() {

  4. return serverSocket.getLocalPort();

  5. }

  6. public void run() {

  7. try {

  8. serverSocket = new ServerSocket(0);

  9. while (true) {

  10. Socket socket = serverSocket.accept();

  11. RequestHandler requestHandler = new RequestHandler(socket);

  12. requestHandler.start();

  13. }

  14. } catch (IOException e) {

  15. e.printStackTrace();

  16. } finally {

  17. if (serverSocket != null) {

  18. try {

  19. serverSocket.close();

  20. } catch (IOException e) {

  21. e.printStackTrace();

  22. }

  23. }

  24. }

  25. }

  26. public static void main(String[] args) throws IOException {

  27. DemoServer server = new DemoServer();

  28. server.start();

  29. try (Socket client = new Socket(InetAddress.getLocalHost(), server.getPort())) {

  30. BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream()));

  31. bufferedReader.lines().forEach(s -> System.out.println(s));

  32. }

  33. }

  34. }

  35. // 简化实现,不做读取,直接发送字符串

  36. class RequestHandler extends Thread {

  37. private Socket socket;

  38. RequestHandler(Socket socket) {

  39. this.socket = socket;

  40. }

  41. @Override

  42. public void run() {

  43. try (PrintWriter out = new PrintWriter(socket.getOutputStream());) {

  44. out.println("Hello world!");

  45. out.flush();

  46. } catch (Exception e) {

  47. e.printStackTrace();

  48. }

  49. }

  50. }

  • 服务器端启动 ServerSocket,端口 0 表示自动绑定一个空闲端口。

  • 调用 accept 方法,阻塞等待客户端连接。

  • 利用 Socket 模拟了一个简单的客户端,只进行连接、读取、打印。

  • 当连接建立后,启动一个单独线程负责回复客户端请求。

这样,一个简单的 Socket 服务器就被实现出来了。

(图片来源于杨晓峰)

三、NIO 代码实现

  1. public class NIOServer extends Thread {

  2. public void run() {

  3. try (Selector selector = Selector.open();

  4. ServerSocketChannel serverSocket = ServerSocketChannel.open();) {// 创建 Selector 和 Channel

  5. serverSocket.bind(new InetSocketAddress(InetAddress.getLocalHost(), 8888));

  6. serverSocket.configureBlocking(false);

  7. // 注册到 Selector,并说明关注点

  8. serverSocket.register(selector, SelectionKey.OP_ACCEPT);

  9. while (true) {

  10. selector.select();// 阻塞等待就绪的 Channel,这是关键点之一

  11. Set<SelectionKey> selectedKeys = selector.selectedKeys();

  12. Iterator<SelectionKey> iter = selectedKeys.iterator();

  13. while (iter.hasNext()) {

  14. SelectionKey key = iter.next();

  15. // 生产系统中一般会额外进行就绪状态检查

  16. sayHelloWorld((ServerSocketChannel) key.channel());

  17. iter.remove();

  18. }

  19. }

  20. } catch (IOException e) {

  21. e.printStackTrace();

  22. }

  23. }

  24. private void sayHelloWorld(ServerSocketChannel server) throws IOException {

  25. try (SocketChannel client = server.accept();) { client.write(Charset.defaultCharset().encode("Hello world!"));

  26. }

  27. }

  28. // 省略了与前面类似的 main

  29. }

  • 首先,通过 Selector.open() 创建一个 Selector,作为类似调度员的角色。

  • 然后,创建一个 ServerSocketChannel,并且向 Selector 注册,通过指定 SelectionKey.OP_ACCEPT,告诉调度员,它关注的是新的连接请求。注意:为什么我们要明确配置非阻塞模式呢?这是因为阻塞模式下,注册操作是不允许的,会抛出 IllegalBlockingModeException 异常。

  • Selector 阻塞在 select 操作,当有 Channel 发生接入请求,就会被唤醒。

  • 在 sayHelloWorld 方法中,通过 SocketChannel 和 Buffer 进行数据操作,在本例中是发送了一段字符串。

可以看到,在前面两个样例中,IO 都是同步阻塞模式,所以需要多线程以实现多任务处理。而 NIO 则是利用了单线程轮询事件的机制,通过高效地定位就绪的 Channel,来决定做什么,仅仅 select 阶段是阻塞的,可以有效避免大量客户端连接时,频繁线程切换带来的问题,应用的扩展能力有了非常大的提高。下面这张图对这种实现思路进行了形象地说明。

(图片来源于杨晓峰)

四、参考资料

杨晓峰《Java核心36讲》

近期热门文章:

Java 最常见的 200+ 面试题


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存