MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java 同步异步与阻塞非阻塞 IO 模型对比解析

2024-11-274.6k 阅读

Java 同步异步与阻塞非阻塞 IO 模型对比解析

在 Java 编程中,IO 操作是非常常见且重要的部分。而同步异步以及阻塞非阻塞这两对概念,对于理解和优化 IO 操作起着关键作用。下面我们将深入探讨它们之间的区别,并通过代码示例来直观感受。

同步与异步

同步和异步主要描述的是任务的执行方式。

同步:在同步操作中,调用方发起一个操作后,必须等待这个操作完成,才能继续执行后续的代码。就好像你在餐厅点餐,点完餐之后必须站在那里等着服务员把餐做好给你,在这个过程中你不能去做其他事情。

异步:而异步操作则不同,调用方发起操作后,无需等待操作完成,就可以继续执行后续代码。操作完成后,系统会通过回调函数、事件通知等方式告知调用方。这类似你在餐厅点餐,点完餐之后你可以去旁边坐着玩手机,等餐好了服务员会叫你。

在 Java 中,很多普通的方法调用都是同步的。例如:

public class SynchronousExample {
    public static void main(String[] args) {
        System.out.println("开始执行同步方法");
        synchronousMethod();
        System.out.println("同步方法执行完毕,继续执行后续代码");
    }

    public static void synchronousMethod() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("同步方法内部操作完成");
    }
}

在上述代码中,main 方法调用 synchronousMethod 时,会阻塞等待 synchronousMethod 执行完毕,也就是等待 3 秒钟后才会打印“同步方法执行完毕,继续执行后续代码”。

而对于异步操作,Java 提供了 FutureCompletableFuture 等工具来实现。以 CompletableFuture 为例:

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class AsynchronousExample {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        System.out.println("开始执行异步操作");
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "异步操作完成";
        });

        System.out.println("异步操作已发起,继续执行后续代码");
        String result = future.get();
        System.out.println(result);
    }
}

在这个例子中,CompletableFuture.supplyAsync 方法异步执行任务,主线程在发起异步操作后可以继续执行后续代码。最后通过 future.get() 获取异步操作的结果,如果异步操作还未完成,get 方法会阻塞等待。

阻塞与非阻塞

阻塞和非阻塞主要关注的是线程在等待操作结果时的状态。

阻塞:当一个线程执行一个阻塞操作时,该线程会被挂起,直到操作完成。例如,一个线程读取文件时,如果文件读取操作是阻塞的,那么在读取完成之前,这个线程不能执行其他任务。

非阻塞:非阻塞操作不会挂起线程,线程在发起操作后,可以立即得到一个状态,表明操作是否完成。如果操作未完成,线程可以继续执行其他任务,然后可以通过轮询等方式再次检查操作状态。

在 Java 的 IO 中,传统的 InputStreamOutputStream 操作默认是阻塞的。例如:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class BlockingIOExample {
    public static void main(String[] args) {
        System.out.println("开始阻塞式 IO 操作");
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) {
            System.out.println("请输入内容:");
            String input = reader.readLine();
            System.out.println("你输入的内容是:" + input);
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("阻塞式 IO 操作完成,继续执行后续代码");
    }
}

在上述代码中,reader.readLine() 是一个阻塞操作。当程序执行到这一行时,线程会被阻塞,等待用户输入内容。只有用户输入并回车后,线程才会继续执行后续代码。

而 Java NIO(New IO)则引入了非阻塞 IO 的概念。以 SocketChannel 为例:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class NonBlockingIOExample {
    public static void main(String[] args) {
        try (SocketChannel socketChannel = SocketChannel.open()) {
            socketChannel.configureBlocking(false);
            socketChannel.connect(new InetSocketAddress("www.baidu.com", 80));
            while (!socketChannel.finishConnect()) {
                System.out.println("正在连接...");
                // 可以在这里执行其他任务
            }
            System.out.println("连接成功");
            ByteBuffer buffer = ByteBuffer.wrap("GET / HTTP/1.1\r\nHost: www.baidu.com\r\n\r\n".getBytes());
            socketChannel.write(buffer);
            buffer.clear();
            int bytesRead = socketChannel.read(buffer);
            while (bytesRead != -1) {
                buffer.flip();
                byte[] data = new byte[buffer.remaining()];
                buffer.get(data);
                System.out.println(new String(data));
                buffer.clear();
                bytesRead = socketChannel.read(buffer);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在这个例子中,通过 socketChannel.configureBlocking(false)SocketChannel 设置为非阻塞模式。在连接服务器时,connect 方法不会阻塞线程,而是立即返回。通过 finishConnect 方法轮询连接状态。读取数据时,read 方法也不会阻塞线程,如果没有数据可读,会立即返回 -1。

同步阻塞 IO(BIO - Blocking I/O)

同步阻塞 IO 是最传统的 IO 模型。在这种模型下,一个线程处理一个连接。当进行 IO 操作时,线程会被阻塞,直到操作完成。

例如,一个简单的服务器端程序,使用 ServerSocket 监听客户端连接,并读取客户端发送的数据:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class BIOServer {
    public static void main(String[] args) {
        try (ServerSocket serverSocket = new ServerSocket(8080)) {
            System.out.println("服务器启动,监听端口 8080");
            while (true) {
                Socket clientSocket = serverSocket.accept();
                System.out.println("客户端连接:" + clientSocket);
                try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                     PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) {
                    String inputLine;
                    while ((inputLine = in.readLine()) != null) {
                        System.out.println("收到客户端消息:" + inputLine);
                        out.println("服务器已收到消息:" + inputLine);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在这个代码中,serverSocket.accept() 方法会阻塞等待客户端连接。当有客户端连接后,in.readLine() 方法又会阻塞等待客户端发送数据。这种模型简单直接,但在高并发场景下,由于每个连接都需要一个线程来处理,会导致大量线程创建和上下文切换开销,性能较低。

同步非阻塞 IO(NIO - Non - Blocking I/O)

同步非阻塞 IO 是 Java 1.4 引入的新 IO 模型。它使用 Selector 来管理多个 Channel。线程可以在多个通道间切换,而不是像 BIO 那样一个线程阻塞在一个通道上。

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

public class NIOServer {
    public static void main(String[] args) {
        try (ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) {
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.bind(new InetSocketAddress(8080));
            Selector selector = Selector.open();
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("服务器启动,监听端口 8080");
            while (true) {
                int readyChannels = selector.select();
                if (readyChannels == 0) continue;
                Set<SelectionKey> selectedKeys = selector.selectedKeys();
                Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
                while (keyIterator.hasNext()) {
                    SelectionKey key = keyIterator.next();
                    if (key.isAcceptable()) {
                        ServerSocketChannel server = (ServerSocketChannel) key.channel();
                        SocketChannel client = server.accept();
                        client.configureBlocking(false);
                        client.register(selector, SelectionKey.OP_READ);
                        System.out.println("客户端连接:" + client);
                    } else if (key.isReadable()) {
                        SocketChannel client = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        int bytesRead = client.read(buffer);
                        if (bytesRead > 0) {
                            buffer.flip();
                            byte[] data = new byte[buffer.remaining()];
                            buffer.get(data);
                            String message = new String(data);
                            System.out.println("收到客户端消息:" + message);
                            ByteBuffer responseBuffer = ByteBuffer.wrap(("服务器已收到消息:" + message).getBytes());
                            client.write(responseBuffer);
                        }
                    }
                    keyIterator.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在这个代码中,ServerSocketChannelSocketChannel 都设置为非阻塞模式。Selector 用于监听通道上的事件(如连接请求、可读事件等)。通过 selector.select() 方法阻塞等待有事件发生,当有事件发生时,遍历 selectedKeys 处理相应的事件。这种模型在高并发场景下,通过一个线程管理多个通道,减少了线程数量和上下文切换开销,提高了性能。

异步阻塞 IO

异步阻塞 IO 在实际应用中相对较少,因为异步操作的特点就是不需要阻塞等待结果。从概念上来说,它是指发起异步操作后,线程仍然被阻塞等待操作完成。这种情况违背了异步操作的初衷,所以在 Java 中并没有典型的异步阻塞 IO 实现。

异步非阻塞 IO(AIO - Asynchronous I/O)

异步非阻塞 IO 是 Java 7 引入的 NIO.2 特性。它与 NIO 的区别在于,NIO 虽然是非阻塞的,但需要通过轮询等方式获取操作结果,而 AIO 是真正的异步,操作完成后会通过回调函数等方式通知调用方。

下面是一个简单的 AIO 服务器端示例:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.nio.channels.ServerSocketChannel;
import java.util.concurrent.ExecutionException;

public class AIOServer {
    private static final int PORT = 8080;

    public static void main(String[] args) {
        try (ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) {
            serverSocketChannel.bind(new InetSocketAddress(PORT));
            serverSocketChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
                @Override
                public void completed(AsynchronousSocketChannel client, Void attachment) {
                    try {
                        serverSocketChannel.accept(null, this);
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        client.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
                            @Override
                            public void completed(Integer result, ByteBuffer buffer) {
                                if (result > 0) {
                                    buffer.flip();
                                    byte[] data = new byte[buffer.remaining()];
                                    buffer.get(data);
                                    String message = new String(data);
                                    System.out.println("收到客户端消息:" + message);
                                    ByteBuffer responseBuffer = ByteBuffer.wrap(("服务器已收到消息:" + message).getBytes());
                                    client.write(responseBuffer, responseBuffer, new CompletionHandler<Integer, ByteBuffer>() {
                                        @Override
                                        public void completed(Integer result, ByteBuffer buffer) {
                                            try {
                                                client.close();
                                            } catch (IOException e) {
                                                e.printStackTrace();
                                            }
                                        }

                                        @Override
                                        public void failed(Throwable exc, ByteBuffer attachment) {
                                            try {
                                                client.close();
                                            } catch (IOException e) {
                                                e.printStackTrace();
                                            }
                                        }
                                    });
                                } else {
                                    try {
                                        client.close();
                                    } catch (IOException e) {
                                        e.printStackTrace();
                                    }
                                }
                            }

                            @Override
                            public void failed(Throwable exc, ByteBuffer attachment) {
                                try {
                                    client.close();
                                } catch (IOException e) {
                                    e.printStackTrace();
                                }
                            }
                        });
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }

                @Override
                public void failed(Throwable exc, Void attachment) {
                    exc.printStackTrace();
                }
            });

            while (true) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在这个代码中,serverSocketChannel.accept 方法以异步方式接受客户端连接,通过 CompletionHandler 处理连接完成后的操作。同样,client.readclient.write 也都是异步操作,通过 CompletionHandler 处理读写完成后的逻辑。这种模型在高并发场景下,性能更高,尤其适合处理大量 I/O 操作的应用。

四种 IO 模型对比总结

  1. 同步阻塞 IO(BIO):简单直观,一个线程处理一个连接,但在高并发场景下性能较差,因为线程会被阻塞,大量线程创建和上下文切换开销大。
  2. 同步非阻塞 IO(NIO):通过 Selector 管理多个通道,减少了线程数量和上下文切换开销,适合高并发场景。但需要手动轮询获取操作结果,编程复杂度较高。
  3. 异步阻塞 IO:违背异步操作初衷,实际应用较少。
  4. 异步非阻塞 IO(AIO):真正的异步操作,操作完成后通过回调通知调用方,性能更高,编程模型更简洁,但对系统和硬件要求较高,应用相对较少。

在实际应用中,需要根据具体场景选择合适的 IO 模型。如果是简单的低并发应用,BIO 可能就足够;如果是高并发应用,NIO 或 AIO 可能更适合。理解这些模型的区别和特点,对于优化 Java 应用的性能至关重要。

通过以上详细的解析和代码示例,希望你对 Java 中的同步异步与阻塞非阻塞 IO 模型有了更深入的理解。在实际开发中,根据具体需求选择合适的模型,能够显著提升程序的性能和效率。