Java同步与异步编程:从阻塞到非阻塞详解
Java同步与异步编程基础:从阻塞到非阻塞的演进之路
1. 同步编程模型
1.1 阻塞式调用(Blocking I/O)
同步编程最显著的特征是线性执行流,当线程执行一个操作时,必须等待该操作完成才能继续执行后续代码。
// 典型的同步文件读取
try (InputStream is = new FileInputStream("data.txt")) {
byte[] buffer = new byte[1024];
int bytesRead = is.read(buffer); // 线程在此阻塞直到数据就绪
String content = new String(buffer, 0, bytesRead);
System.out.println(content);
}
阻塞I/O的问题:
- 线程在等待期间无法执行其他任务
- 高并发场景下需要大量线程,消耗系统资源
- 容易导致线程饥饿或死锁
1.2 线程同步机制
synchronized关键字
public class Counter {
private int count;
// 同步方法
public synchronized void increment() {
count++;
}
// 同步代码块
public void decrement() {
synchronized(this) {
count--;
}
}
}
wait/notify机制
public class MessageQueue {
private final Queue<String> queue = new LinkedList<>();
public synchronized void put(String message) {
queue.add(message);
notify(); // 唤醒等待线程
}
public synchronized String take() throws InterruptedException {
while (queue.isEmpty()) {
wait(); // 释放锁并等待
}
return queue.remove();
}
}
实践建议:
- 优先使用
synchronized
而非wait/notify
,除非需要精细控制 - 同步块应尽量缩小范围,减少锁竞争
- 避免在同步块中执行耗时操作
2. 异步编程模型
2.1 回调函数(Callbacks)
public interface Callback {
void onSuccess(String result);
void onFailure(Throwable t);
}
public class FileReader {
public void readAsync(String path, Callback callback) {
new Thread(() -> {
try {
String content = syncReadFile(path);
callback.onSuccess(content);
} catch (IOException e) {
callback.onFailure(e);
}
}).start();
}
private String syncReadFile(String path) throws IOException {
// 同步读取实现
}
}
回调地狱问题:
serviceA.call(param, resultA -> {
serviceB.call(resultA, resultB -> {
serviceC.call(resultB, resultC -> {
// 嵌套层级越来越深
});
});
});
2.2 事件驱动(Event Loop)
Java中的事件循环实现:
public class EventLoop {
private final Queue<Runnable> eventQueue = new ConcurrentLinkedQueue<>();
private volatile boolean running = true;
public void start() {
new Thread(() -> {
while (running) {
Runnable task = eventQueue.poll();
if (task != null) {
task.run();
}
}
}).start();
}
public void submit(Runnable task) {
eventQueue.offer(task);
}
}
2.3 非阻塞I/O(NIO)
Java NIO核心组件:
- Channel:替代传统InputStream/OutputStream
- Buffer:数据容器
- Selector:多路复用器
// NIO文件复制示例
try (FileChannel src = new FileInputStream("source.txt").getChannel();
FileChannel dest = new FileOutputStream("dest.txt").getChannel()) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (src.read(buffer) != -1) {
buffer.flip(); // 切换为读模式
dest.write(buffer);
buffer.clear(); // 清空缓冲区
}
}
实践建议:
- 对于I/O密集型应用,优先考虑NIO
- 使用Selector时注意正确处理OP_WRITE事件
- 结合内存池技术减少Buffer创建开销
3. 同步与异步对比
特性 | 同步编程 | 异步编程 |
---|---|---|
执行流程 | 线性顺序执行 | 事件驱动,非顺序 |
线程使用 | 每个请求独占线程 | 少量线程处理大量请求 |
代码复杂度 | 简单直观 | 回调嵌套导致复杂度高 |
适用场景 | CPU密集型任务 | I/O密集型任务 |
资源消耗 | 线程数随请求线性增长 | 固定线程池 |
典型实现 | 传统Servlet | Netty, Node.js |
4. 演进趋势与选择建议
- 混合模式:现代框架往往采用同步编程模型+异步底层实现(如Spring WebFlux)
- 虚拟线程:Project Loom引入的轻量级线程可能改变同步/异步的边界
选择原则:
- 简单业务逻辑 → 同步
- 高并发I/O操作 → 异步
- 计算密集型任务 → 同步+多线程
性能优化方向:
- 同步:减少锁竞争(锁粗化、锁消除)
- 异步:避免回调地狱(使用CompletableFuture或响应式编程)
掌握同步/异步编程的核心差异,能够根据实际场景做出合理选择,是构建高性能Java应用的基础能力。