并发让多个任务在时间上重叠,吞吐能否提高还取决于 CPU、数据库连接、网络和任务依赖。开一万个线程或 goroutine,只是把等待搬到别处;如果数据库只能处理几十个并发查询,其余任务仍占用内存和请求预算。

先定位真正共享的状态

count++ 包含读取、加一、写回。两个执行者都读到 10,就可能都写回 11。锁保护的是一组必须共同成立的操作,不是把某一个变量声明成“线程安全”就自动保护所有业务规则。

type Counter struct {
    mu sync.Mutex
    n  int
}

func (c *Counter) Add() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.n++
}

这段代码需导入 sync;保护同一状态的所有读写都必须遵守相同规则。计数器可以用原子操作,但“检查库存后扣减”涉及业务不变量,不应随意拆成几个彼此独立的原子读写。在 Go 中,包含 Mutex 的值投入使用后不要再复制;用指针传递。

Java 中的 volatile 解决特定可见性与有序性问题,并不把 count++ 变成原子操作。可以用 AtomicInteger.incrementAndGet(),或在 synchronized 中保护相关状态。线程安全集合也不一定让“先检查、再插入”的组合操作原子化。

有界队列把过载变成可处理的结果

一个服务有 4 个工作协程和容量 16 的队列:最多约有 4 个执行中任务加 16 个等待任务。提交时队列已满,就拒绝、限时等待或让上游减速;绝不能悄悄无限扩容。异步任务接口可返回 503 或明确的忙碌结果,让客户端按预算重试。

select {
case queue <- job:
    // 已接收;仍需定义断电后任务是否丢失。
case <-ctx.Done():
    return ctx.Err()
default:
    return ErrQueueFull
}

这是逻辑片段,完整可运行实现见 Go 任务服务。队列有界只保护内存,不代表任务持久化。对必须恢复的任务,需要把接收确认与持久存储关联起来。

超时与取消是一条传播链

HTTP 请求有总预算,下游数据库和远程调用必须分到更短的预算。Go 向下传递 context,调用取消函数,并在等待与循环中检查 Done;不能为了后台继续执行就无条件改成 context.Background。Java 取消 Future 不保证所有底层调用立即停止,需配合库提供的超时、中断响应和资源关闭。

Java 21 的虚拟线程适合大量等待 I/O 的任务,可以降低线程管理成本,但不会让 CPU 计算自动变快,也不会增加数据库连接数。连接池、信号量和请求上限仍然需要独立配置。CPU 密集任务应基于核数、任务大小和实测来限制并行度。

排查顺序

服务变慢时先看队列长度与等待时间,再区分执行耗时、锁等待、连接池等待和 CPU。只看 CPU 低就增加并发,可能进一步压垮下游。用线程转储或 goroutine profile 找阻塞位置,用火焰图看 CPU 消耗;二者回答不同问题。

练习与面试自查

运行 Go 任务工程的取消与队列满测试。把处理器故意停住,确认第一个正在执行、第二个等待、第三个被拒绝,恢复处理后服务可以继续接受请求。这个实验应通过同步屏障控制顺序。

  • channel 能否自动消除一切共享内存竞争?什么时候互斥锁更直接?
  • 高并发、低延迟和高吞吐之间有什么约束?用连接池饱和举例。
  • 超时返回后后台任务还在运行,会造成哪些资源与业务问题?

参考资料