Java 同步异步与阻塞非阻塞 IO 模型对比解析
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 提供了 Future
、CompletableFuture
等工具来实现。以 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 中,传统的 InputStream
和 OutputStream
操作默认是阻塞的。例如:
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();
}
}
}
在这个代码中,ServerSocketChannel
和 SocketChannel
都设置为非阻塞模式。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.read
和 client.write
也都是异步操作,通过 CompletionHandler
处理读写完成后的逻辑。这种模型在高并发场景下,性能更高,尤其适合处理大量 I/O 操作的应用。
四种 IO 模型对比总结
- 同步阻塞 IO(BIO):简单直观,一个线程处理一个连接,但在高并发场景下性能较差,因为线程会被阻塞,大量线程创建和上下文切换开销大。
- 同步非阻塞 IO(NIO):通过
Selector
管理多个通道,减少了线程数量和上下文切换开销,适合高并发场景。但需要手动轮询获取操作结果,编程复杂度较高。 - 异步阻塞 IO:违背异步操作初衷,实际应用较少。
- 异步非阻塞 IO(AIO):真正的异步操作,操作完成后通过回调通知调用方,性能更高,编程模型更简洁,但对系统和硬件要求较高,应用相对较少。
在实际应用中,需要根据具体场景选择合适的 IO 模型。如果是简单的低并发应用,BIO 可能就足够;如果是高并发应用,NIO 或 AIO 可能更适合。理解这些模型的区别和特点,对于优化 Java 应用的性能至关重要。
通过以上详细的解析和代码示例,希望你对 Java 中的同步异步与阻塞非阻塞 IO 模型有了更深入的理解。在实际开发中,根据具体需求选择合适的模型,能够显著提升程序的性能和效率。