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)

图1

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密集型任务
资源消耗线程数随请求线性增长固定线程池
典型实现传统ServletNetty, Node.js

4. 演进趋势与选择建议

  1. 混合模式:现代框架往往采用同步编程模型+异步底层实现(如Spring WebFlux)
  2. 虚拟线程:Project Loom引入的轻量级线程可能改变同步/异步的边界
  3. 选择原则

    • 简单业务逻辑 → 同步
    • 高并发I/O操作 → 异步
    • 计算密集型任务 → 同步+多线程

性能优化方向

  • 同步:减少锁竞争(锁粗化、锁消除)
  • 异步:避免回调地狱(使用CompletableFuture或响应式编程)

掌握同步/异步编程的核心差异,能够根据实际场景做出合理选择,是构建高性能Java应用的基础能力。

添加新评论