rokevin
移动
前端
语言
  • 基础

    • Linux
    • 实施
    • 版本构建
  • 应用

    • WEB服务器
    • 数据库
  • 资讯

    • 工具
    • 部署
开放平台
产品设计
  • 人工智能
  • 云计算
计算机
其它
GitHub
移动
前端
语言
  • 基础

    • Linux
    • 实施
    • 版本构建
  • 应用

    • WEB服务器
    • 数据库
  • 资讯

    • 工具
    • 部署
开放平台
产品设计
  • 人工智能
  • 云计算
计算机
其它
GitHub
  • Java 并发 - 理论基础

  • 进程和线程
  • 带着BAT大厂的面试问题去理解
  • 为什么需要多线程
  • 线程不安全示例
  • 并发出现问题的根源: 并发三要素
    • 可见性: CPU缓存引起
    • 原子性: 分时复用引起
    • 有序性: 重排序引起
  • JAVA是怎么解决并发问题的: JMM(Java内存模型)
    • 关键字: volatile、synchronized 和 final
    • Happens-Before 规则
      • 1. 单一线程原则
      • 2. 管程锁定规则
      • 3. volatile 变量规则
      • 4. 线程启动规则
      • 5. 线程加入规则
      • 6. 线程中断规则
      • 7. 对象终结规则
      • 8. 传递性
  • 线程安全: 不是一个非真即假的命题
    • 1. 不可变
    • 2. 绝对线程安全
    • 3. 相对线程安全
    • 4. 线程兼容
    • 5. 线程对立
  • 线程安全的实现方法
    • 1. 互斥同步
    • 2. 非阻塞同步
    • 3. 无同步方案
  • 高并发
    • 高并发编程的核心意义
    • 高并发编程的核心好处
    • 高并发编程的核心注意事项(避坑关键)
      • 1. 并发安全:避免数据竞争与线程安全问题
      • 2. 资源管控:防止资源耗尽
      • 3. 异步编程:处理回调地狱与状态同步
      • 4. 锁的合理使用:避免死锁与性能损耗
      • 5. 可见性与有序性:避免指令重排与缓存一致性问题
      • 6. I/O 与网络:处理异步 I/O 与网络波动
      • 7. 性能监控与调优:避免盲目优化
      • 8. 业务层面:避免并发场景的业务逻辑漏洞
  • CPU核心数和线程数之间的关系
    • 核心概念区分
    • 核心数与线程数的核心关系
    • 关键补充(避免误解)
    • 核心配置逻辑(先懂原理再用)
    • 不同 CPU 配置的实战配置表(直接套用)
      • 关键补充说明:
    • 不同语言的线程池配置示例(落地代码)
      • 1. Java(ThreadPoolExecutor)
      • 2. Go(goroutine + worker pool)
      • 3. Python(ThreadPoolExecutor)
    • 调优技巧(避免踩坑)
    • 总结
  • CPU时间片轮转机制
    • 核心原理
    • 关键细节(理解机制的核心)
    • 核心作用(为什么需要这个机制)
    • 和高并发编程的关联(避坑关键)
  • 时间片轮转机制下的线程数优化实战
    • 先明确 3 个核心前提(优化的基础)
      • 1. 确定 CPU 核心配置(关键参数)
      • 2. 区分任务类型(核心决策依据)
      • 3. 理解时间片轮转的约束
    • 通用线程数计算公式(直接套用)
      • 1. CPU 密集型任务(无阻塞 / 低阻塞)
      • 2. I/O 密集型任务(高阻塞)
      • 3. 混合任务(部分 CPU 密集 + 部分 I/O 密集)
    • 不同 CPU 配置的实战配置表(落地即用)
      • 配置说明:
    • 不同语言的线程数配置实战示例
      • 1. Java(ThreadPoolExecutor)
      • 2. Go(Goroutine Worker Pool)
    • 3. Python(ThreadPoolExecutor/ProcessPoolExecutor)
    • 优化验证与调优技巧(避免踩坑)
      • 1. 核心监控指标(必看)
      • 2. 调优步骤(迭代优化)
      • 3. 避坑指南
    • 总结
  • C10K 问题
    • 本质、解决方案与现代演进
    • C10K 问题的本质:为什么早期服务器扛不住 1 万并发?
    • 解决 C10K 的核心思路:从 “多线程” 到 “I/O 多路复用”
      • 1. 三大经典 I/O 多路复用技术(跨平台核心方案)
      • 2. 配套优化:突破系统限制
    • C10K 的经典解决方案架构
    • C10K 的演进:从 C10K 到 C10M/C100M
    • 和高并发编程的关联(实战避坑)
    • 总结
  • 其它高并发场景问题
    • 按 “并发规模演进” 的核心问题(从 C10K 到 C100M)
      • 1. C10M 问题(1000 万并发连接)
      • 2. C100M 问题(1 亿并发连接)
      • 3. C100K 问题(10 万并发请求)
    • 按 “场景细分” 的高并发问题(聚焦特定瓶颈)
      • 1. C10K/C100K 写并发(写密集型高并发)
      • 2. 热点数据高并发访问(读密集型高并发)
      • 3. 高并发下的延迟敏感问题(低延迟高并发)
      • 4. 分布式高并发问题(跨节点高并发)
      • 5. 峰值流量冲击问题(突发高并发)
    • 核心总结:所有高并发问题的本质与通用解法

Java 并发 - 理论基础

本文从理论的角度引入并发安全问题以及JMM应对并发问题的原理。

并行CPU的核数或者超核线程

并发跟单位时间有关,时间片轮转机制

高并发编程意义、好处和注意事项

进程和线程

进程:操作系统分配资源的最小单位

线程:CPU调度的最小单位

启动一个线程,栈控件会分配1M内存空间

上下文切换就是CPU时间片切换 耗费20000万时间周期 比较消耗CPU资源

Linux 最多线程不能超过1000 Windows 最不线程数不能超过2000

带着BAT大厂的面试问题去理解

提示

请带着这些问题继续后文,会很大程度上帮助你更好的理解并发理论基础。

  • 多线程的出现是要解决什么问题的?
  • 线程不安全是指什么? 举例说明
  • 并发出现线程不安全的本质什么? 可见性,原子性和有序性。
  • Java是怎么解决并发问题的? 3个关键字,JMM和8个Happens-Before
  • 线程安全是不是非真即假? 不是
  • 线程安全有哪些实现思路?
  • 如何理解并发和并行的区别?

为什么需要多线程

众所周知,CPU、内存、I/O 设备的速度是有极大差异的,为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献,主要体现为:

  • CPU 增加了缓存,以均衡与内存的速度差异;// 导致 可见性问题
  • 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;// 导致 原子性问题
  • 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。// 导致 有序性问题

线程不安全示例

如果多个线程对同一个共享数据进行访问而不采取同步操作的话,那么操作的结果是不一致的。

以下代码演示了 1000 个线程同时对 cnt 执行自增操作,操作结束之后它的值有可能小于 1000。

public class ThreadUnsafeExample {

    private int cnt = 0;

    public void add() {
        cnt++;
    }

    public int get() {
        return cnt;
    }
}
public static void main(String[] args) throws InterruptedException {
    final int threadSize = 1000;
    ThreadUnsafeExample example = new ThreadUnsafeExample();
    final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < threadSize; i++) {
        executorService.execute(() -> {
            example.add();
            countDownLatch.countDown();
        });
    }
    countDownLatch.await();
    executorService.shutdown();
    System.out.println(example.get());
}
997 // 结果总是小于1000

并发出现问题的根源: 并发三要素

上述代码输出为什么不是1000? 并发出现问题的根源是什么?

可见性: CPU缓存引起

可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到。

举个简单的例子,看下面这段代码:

//线程1执行的代码
int i = 0;
i = 10;
 
//线程2执行的代码
j = i;

假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。

此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10.

这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。

原子性: 分时复用引起

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

举个简单的例子,看下面这段代码:

int i = 1;

// 线程1执行
i += 1;

// 线程2执行
i += 1;

这里需要注意的是:i += 1需要三条 CPU 指令

  1. 将变量 i 从内存读取到 CPU寄存器;
  2. 在CPU寄存器中执行 i + 1 操作;
  3. 将最后的结果i写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)。

由于CPU分时复用(线程切换)的存在,线程1执行了第一条指令后,就切换到线程2执行,假如线程2执行了这三条指令后,再切换会线程1执行后续两条指令,将造成最后写到内存中的i值是2而不是3。

有序性: 重排序引起

有序性:即程序执行的顺序按照代码的先后顺序执行。举个简单的例子,看下面这段代码:

int i = 0;              
boolean flag = false;
i = 1;                //语句1  
flag = true;          //语句2

上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗? 不一定,为什么呢? 这里可能会发生指令重排序(Instruction Reorder)。

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:

  • 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  • 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  • 内存系统的重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

从 java 源代码到最终实际执行的指令序列,会分别经历下面三种重排序:

上述的 1 属于编译器重排序,2 和 3 属于处理器重排序。这些重排序都可能会导致多线程程序出现内存可见性问题。对于编译器,JMM 的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM 的处理器重排序规则会要求 java 编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers,intel 称之为 memory fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。

具体可以参看:Java 内存模型详解的重排序章节。

JAVA是怎么解决并发问题的: JMM(Java内存模型)

Java 内存模型是个很复杂的规范,强烈推荐你看后续(应该是网上能找到最好的材料之一了):Java 内存模型详解。

理解的第一个维度:核心知识点

JMM本质上可以理解为,Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。具体来说,这些方法包括:

  • volatile、synchronized 和 final 三个关键字
  • Happens-Before 规则

理解的第二个维度:可见性,有序性,原子性

  • 原子性

在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。 请分析以下哪些操作是原子性操作:

x = 10;        //语句1: 直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中
y = x;         //语句2: 包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。
x++;           //语句3: x++包括3个操作:读取x的值,进行加1操作,写入新的值。
x = x + 1;     //语句4: 同语句3

上面4个语句只有语句1的操作具备原子性。

也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。

从上面可以看出,Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。

  • 可见性

Java提供了volatile关键字来保证可见性。

当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

  • 有序性

在Java里面,可以通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲述)。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。当然JMM是通过Happens-Before 规则来保证有序性的。

关键字: volatile、synchronized 和 final

以下三篇文章详细分析了这三个关键字:

  • 关键字: synchronized详解
  • 关键字: volatile详解
  • 关键字: final详解

Happens-Before 规则

上面提到了可以用 volatile 和 synchronized 来保证有序性。除此之外,JVM 还规定了先行发生原则,让一个操作无需控制就能先于另一个操作完成。

1. 单一线程原则

Single Thread rule

在一个线程内,在程序前面的操作先行发生于后面的操作。

2. 管程锁定规则

Monitor Lock Rule

一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。

3. volatile 变量规则

Volatile Variable Rule

对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。

4. 线程启动规则

Thread Start Rule

Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。

5. 线程加入规则

Thread Join Rule

Thread 对象的结束先行发生于 join() 方法返回。

6. 线程中断规则

Thread Interruption Rule

对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 interrupted() 方法检测到是否有中断发生。

7. 对象终结规则

Finalizer Rule

一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。

8. 传递性

Transitivity

如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C。

线程安全: 不是一个非真即假的命题

一个类在可以被多个线程安全调用时就是线程安全的。

线程安全不是一个非真即假的命题,可以将共享数据按照安全程度的强弱顺序分成以下五类: 不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。

1. 不可变

不可变(Immutable)的对象一定是线程安全的,不需要再采取任何的线程安全保障措施。只要一个不可变的对象被正确地构建出来,永远也不会看到它在多个线程之中处于不一致的状态。

多线程环境下,应当尽量使对象成为不可变,来满足线程安全。

不可变的类型:

  • final 关键字修饰的基本数据类型
  • String
  • 枚举类型
  • Number 部分子类,如 Long 和 Double 等数值包装类型,BigInteger 和 BigDecimal 等大数据类型。但同为 Number 的原子类 AtomicInteger 和 AtomicLong 则是可变的。

对于集合类型,可以使用 Collections.unmodifiableXXX() 方法来获取一个不可变的集合。

public class ImmutableExample {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        Map<String, Integer> unmodifiableMap = Collections.unmodifiableMap(map);
        unmodifiableMap.put("a", 1);
    }
}
Exception in thread "main" java.lang.UnsupportedOperationException
    at java.util.Collections$UnmodifiableMap.put(Collections.java:1457)
    at ImmutableExample.main(ImmutableExample.java:9)

Collections.unmodifiableXXX() 先对原始的集合进行拷贝,需要对集合进行修改的方法都直接抛出异常。

public V put(K key, V value) {
    throw new UnsupportedOperationException();
}

2. 绝对线程安全

不管运行时环境如何,调用者都不需要任何额外的同步措施。

3. 相对线程安全

相对线程安全需要保证对这个对象单独的操作是线程安全的,在调用的时候不需要做额外的保障措施。但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。

在 Java 语言中,大部分的线程安全类都属于这种类型,例如 Vector、HashTable、Collections 的 synchronizedCollection() 方法包装的集合等。

对于下面的代码,如果删除元素的线程删除了 Vector 的一个元素,而获取元素的线程试图访问一个已经被删除的元素,那么就会抛出 ArrayIndexOutOfBoundsException。

public class VectorUnsafeExample {
    private static Vector<Integer> vector = new Vector<>();

    public static void main(String[] args) {
        while (true) {
            for (int i = 0; i < 100; i++) {
                vector.add(i);
            }
            ExecutorService executorService = Executors.newCachedThreadPool();
            executorService.execute(() -> {
                for (int i = 0; i < vector.size(); i++) {
                    vector.remove(i);
                }
            });
            executorService.execute(() -> {
                for (int i = 0; i < vector.size(); i++) {
                    vector.get(i);
                }
            });
            executorService.shutdown();
        }
    }
}
Exception in thread "Thread-159738" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 3
    at java.util.Vector.remove(Vector.java:831)
    at VectorUnsafeExample.lambda$main$0(VectorUnsafeExample.java:14)
    at VectorUnsafeExample$$Lambda$1/713338599.run(Unknown Source)
    at java.lang.Thread.run(Thread.java:745)

如果要保证上面的代码能正确执行下去,就需要对删除元素和获取元素的代码进行同步。

executorService.execute(() -> {
    synchronized (vector) {
        for (int i = 0; i < vector.size(); i++) {
            vector.remove(i);
        }
    }
});
executorService.execute(() -> {
    synchronized (vector) {
        for (int i = 0; i < vector.size(); i++) {
            vector.get(i);
        }
    }
});

4. 线程兼容

线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用,我们平常说一个类不是线程安全的,绝大多数时候指的是这一种情况。Java API 中大部分的类都是属于线程兼容的,如与前面的 Vector 和 HashTable 相对应的集合类 ArrayList 和 HashMap 等。

5. 线程对立

线程对立是指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。由于 Java 语言天生就具备多线程特性,线程对立这种排斥多线程的代码是很少出现的,而且通常都是有害的,应当尽量避免。

线程安全的实现方法

1. 互斥同步

synchronized 和 ReentrantLock。

初步了解你可以看:

  • Java 并发 - 线程基础:线程互斥同步

详细分析请看:

  • 关键字: Synchronized详解
  • JUC锁: ReentrantLock详解

2. 非阻塞同步

互斥同步最主要的问题就是线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。

互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题。无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。

(一)CAS

随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略: 先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不需要将线程阻塞,因此这种同步操作称为非阻塞同步。

乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。硬件支持的原子性操作最典型的是: 比较并交换(Compare-and-Swap,CAS)。CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。

(二)AtomicInteger

J.U.C 包里面的整数原子类 AtomicInteger,其中的 compareAndSet() 和 getAndIncrement() 等方法都使用了 Unsafe 类的 CAS 操作。

以下代码使用了 AtomicInteger 执行了自增的操作。

private AtomicInteger cnt = new AtomicInteger();

public void add() {
    cnt.incrementAndGet();
}

以下代码是 incrementAndGet() 的源码,它调用了 unsafe 的 getAndAddInt() 。

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

以下代码是 getAndAddInt() 源码,var1 指示对象内存地址,var2 指示该字段相对对象内存地址的偏移,var4 指示操作需要加的数值,这里为 1。通过 getIntVolatile(var1, var2) 得到旧的预期值,通过调用 compareAndSwapInt() 来进行 CAS 比较,如果该字段内存地址中的值等于 var5,那么就更新内存地址为 var1+var2 的变量为 var5+var4。

可以看到 getAndAddInt() 在一个循环中进行,发生冲突的做法是不断的进行重试。

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

(三)ABA

如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。

J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。

CAS, Unsafe和原子类详细分析请看:

  • JUC原子类: CAS, Unsafe和原子类详解

3. 无同步方案

要保证线程安全,并不是一定就要进行同步。如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性。

(一)栈封闭

多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class StackClosedExample {
    public void add100() {
        int cnt = 0;
        for (int i = 0; i < 100; i++) {
            cnt++;
        }
        System.out.println(cnt);
    }
}
public static void main(String[] args) {
    StackClosedExample example = new StackClosedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> example.add100());
    executorService.execute(() -> example.add100());
    executorService.shutdown();
}
100
100

更详细的分析请看J.U.C中线程池相关内容详解:

  • JUC线程池: FutureTask详解
  • JUC线程池: ThreadPoolExecutor详解
  • JUC线程池: ScheduledThreadPool详解
  • JUC线程池: Fork/Join框架详解

(二)线程本地存储(Thread Local Storage)

如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。

符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完。其中最重要的一个应用实例就是经典 Web 交互模型中的“一个请求对应一个服务器线程”(Thread-per-Request)的处理方式,这种处理方式的广泛应用使得很多 Web 服务端应用都可以使用线程本地存储来解决线程安全问题。

可以使用 java.lang.ThreadLocal 类来实现线程本地存储功能。

对于以下代码,thread1 中设置 threadLocal 为 1,而 thread2 设置 threadLocal 为 2。过了一段时间之后,thread1 读取 threadLocal 依然是 1,不受 thread2 的影响。

public class ThreadLocalExample {
    public static void main(String[] args) {
        ThreadLocal threadLocal = new ThreadLocal();
        Thread thread1 = new Thread(() -> {
            threadLocal.set(1);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(threadLocal.get());
            threadLocal.remove();
        });
        Thread thread2 = new Thread(() -> {
            threadLocal.set(2);
            threadLocal.remove();
        });
        thread1.start();
        thread2.start();
    }
}

输出结果

1

为了理解 ThreadLocal,先看以下代码:

public class ThreadLocalExample1 {
    public static void main(String[] args) {
        ThreadLocal threadLocal1 = new ThreadLocal();
        ThreadLocal threadLocal2 = new ThreadLocal();
        Thread thread1 = new Thread(() -> {
            threadLocal1.set(1);
            threadLocal2.set(1);
        });
        Thread thread2 = new Thread(() -> {
            threadLocal1.set(2);
            threadLocal2.set(2);
        });
        thread1.start();
        thread2.start();
    }
}

它所对应的底层结构图为:

每个 Thread 都有一个 ThreadLocal.ThreadLocalMap 对象,Thread 类中就定义了 ThreadLocal.ThreadLocalMap 成员。

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

当调用一个 ThreadLocal 的 set(T value) 方法时,先得到当前线程的 ThreadLocalMap 对象,然后将 ThreadLocal->value 键值对插入到该 Map 中。

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

get() 方法类似。

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因为根本不存在多线程竞争。

在一些场景 (尤其是使用线程池) 下,由于 ThreadLocal.ThreadLocalMap 的底层数据结构导致 ThreadLocal 有内存泄漏的情况,应该尽可能在每次使用 ThreadLocal 后手动调用 remove(),以避免出现 ThreadLocal 经典的内存泄漏甚至是造成自身业务混乱的风险。

更详细的分析看:Java 并发 - ThreadLocal详解

(三)可重入代码(Reentrant Code)

这种代码也叫做纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。

可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。

高并发

高并发编程是指设计能同时处理大量并发请求 / 任务的程序(通常涉及多线程、多进程、异步 I/O 等技术),核心目标是提升系统的 “并发处理能力” 和 “资源利用率”,是应对现代分布式系统、互联网应用(如电商秒杀、直播互动、支付系统)的关键技术。

高并发编程的核心价值是在有限资源下提升系统吞吐、降低延迟、满足海量请求需求,但代价是引入了并发安全、资源管控、异步同步等复杂问题。实践中需遵循 “先保证正确性,再追求性能” 的原则:优先通过无状态设计、池化技术、异步 I/O 降低复杂度,再通过锁、分布式锁解决并发安全,最后通过监控调优提升性能,避免为了 “高并发” 而过度设计。

高并发编程的核心意义

本质是解决 “有限资源” 与 “海量请求” 的矛盾,具体意义体现在 3 个层面:

  1. 匹配硬件发展趋势:现代 CPU 多为多核架构,串行编程无法充分利用多核资源(相当于 “多车道公路只开一条道”),高并发编程能让多个核心同时工作,释放硬件性能;
  2. 满足业务场景需求:互联网应用需应对 “峰值流量”(如双 11 秒杀、直播带货下单),高并发编程可避免系统在海量请求下瘫痪或响应迟缓;
  3. 提升系统吞吐与响应速度:通过 “并行处理”(多任务同时执行)或 “异步非阻塞”(减少等待时间),解决串行编程中 “一个任务阻塞导致整体卡顿” 的问题。

高并发编程的核心好处

  1. 提升系统吞吐量(Throughput)

    吞吐量指单位时间内处理的请求 / 任务数。例如:串行处理 1000 个请求需 10 秒,而并发处理可能仅需 2 秒,吞吐量直接提升 5 倍(尤其适用于数据批量处理、消息队列消费等场景)。

  2. 降低响应延迟(Latency)

    响应延迟指单个请求从发起到完成的时间。通过 “异步化”(如 HTTP 异步请求、数据库异步查询)避免 “同步等待”,例如:用户下单时,无需等待库存扣减、日志记录、短信通知全部完成再返回结果,而是异步并行执行,响应时间从 500ms 降至 100ms。

  3. 提高资源利用率

    避免 CPU “空闲等待”—— 例如:I/O 密集型任务(网络请求、数据库查询、文件读写)中,CPU 大部分时间在等待 I/O 完成,并发编程可让 CPU 在等待期间处理其他任务(如一个线程等数据库响应时,另一个线程处理新请求),使 CPU、内存、网络等资源充分利用。

  4. 增强系统扩展性与容错性

    高并发设计通常伴随 “解耦”(如线程池、异步队列、分布式架构),可通过横向扩展(增加机器 / 实例)进一步提升并发能力;同时,单个任务 / 线程故障不会直接导致整个系统崩溃(通过隔离、重试机制兜底)。

  5. 优化用户体验

    对交互类应用(如 APP、网页),低延迟意味着 “操作无卡顿”(如点击按钮立即反馈),高吞吐量意味着 “峰值期不崩溃”(如秒杀时能正常下单),直接提升用户留存率。

高并发编程的核心注意事项(避坑关键)

高并发带来性能提升的同时,也引入了 “并发安全”“资源竞争” 等复杂问题,需重点关注以下 8 点:

1. 并发安全:避免数据竞争与线程安全问题

  • 核心风险:多个线程同时读写共享资源(如全局变量、静态变量、数据库记录),会导致数据不一致(如库存超卖、余额计算错误)、死锁、活锁等问题。
  • 解决方案:
    • 无状态设计:尽量避免共享资源(如每个请求独立创建局部变量,而非使用全局变量);
    • 原子操作:使用原子类(如 Java 的AtomicInteger、Go 的sync/atomic)处理简单数值更新;
    • 锁机制:复杂场景用互斥锁(如 Java 的synchronized、Go 的sync.Mutex)或读写锁(ReentrantReadWriteLock),注意锁粒度(避免 “大锁” 导致并发降级);
    • 不可变对象:共享数据用不可变对象(如 Java 的String),避免修改操作。

2. 资源管控:防止资源耗尽

  • 核心风险:无限制创建线程 / 进程(每个线程占用栈内存、CPU 时间片),或无限制接收请求,会导致 OOM(内存溢出)、CPU 使用率 100%、系统宕机。
  • 解决方案:
    • 线程池 / 连接池:使用线程池(如 Java 的ThreadPoolExecutor、Go 的worker pool)控制线程数量,数据库连接池(如 HikariCP)控制连接数,避免资源无限扩张;
    • 限流熔断:通过限流(如令牌桶、漏桶算法)限制单位时间内的请求数,通过熔断(如 Sentinel、Hystrix)避免故障服务拖垮整个系统;
    • 资源隔离:不同业务(如支付、商品)使用独立的线程池 / 资源池,避免单个业务异常影响全局。

3. 异步编程:处理回调地狱与状态同步

  • 核心风险:异步编程(如回调函数、Promise、协程)中,任务执行顺序不确定,容易出现 “回调地狱”(代码可读性差、难以维护),或异步任务间的状态同步错误(如 A 任务未完成,B 任务已读取其未更新的数据)。
  • 解决方案:
    • 用高级异步工具:避免嵌套回调,使用 Promise/async-await(JS)、CompletableFuture(Java)、goroutine+channel(Go)等简化异步逻辑;
    • 明确状态依赖:通过信号量、CountDownLatch 等工具确保异步任务的执行顺序(如 “所有子任务完成后再执行汇总任务”);
    • 日志与追踪:通过链路追踪(如 SkyWalking、Zipkin)记录异步任务的调用链路,便于问题排查。

4. 锁的合理使用:避免死锁与性能损耗

  • 核心风险:
    • 死锁:多个线程互相持有对方需要的锁(如线程 A 持有锁 1,等待锁 2;线程 B 持有锁 2,等待锁 1);
    • 锁竞争激烈:高并发下大量线程争抢同一把锁,导致线程阻塞、上下文切换频繁,反而降低性能(“并发降级为串行”)。
  • 解决方案:
    • 避免死锁:按固定顺序获取锁、设置锁超时时间(如 Java 的tryLock(timeout))、定期释放锁;
    • 优化锁粒度:将 “大锁” 拆分为 “小锁”(如 HashMap 改为 ConcurrentHashMap,分段锁减少竞争);
    • 无锁编程:优先使用无锁数据结构(如 ConcurrentLinkedQueue)或 CAS(Compare-And-Swap)机制,减少锁依赖。

5. 可见性与有序性:避免指令重排与缓存一致性问题

  • 核心风险:CPU 缓存、指令重排会导致 “线程 A 修改的数据,线程 B 看不到”(可见性问题),或 “代码执行顺序与预期不一致”(有序性问题),例如:

    // 线程A:初始化完成
    boolean initialized = true;
    // 线程B:可能看到initialized=true,但data未初始化(指令重排导致)
    if (initialized) {
        System.out.println(data); 
    }
    
  • 解决方案:

    • 用 volatile 关键字(Java):保证变量可见性和禁止指令重排;
    • 用锁或原子类:锁的获取 / 释放会隐式保证可见性和有序性;
    • 避免依赖 “happen-before” 规则:不依赖 JVM/CPU 的默认内存模型,通过显式同步机制确保逻辑正确性。

6. I/O 与网络:处理异步 I/O 与网络波动

  • 核心风险:高并发场景中,I/O 操作(数据库、Redis、网络请求)是瓶颈,同步 I/O 会导致线程阻塞,网络波动(超时、重试)会加剧并发控制难度。
  • 解决方案:
    • 异步 I/O 优先:使用异步数据库客户端(如 Java 的 R2DBC)、异步 HTTP 客户端(如 OkHttp 异步模式),减少线程阻塞;
    • 超时与重试:所有 I/O 操作设置超时时间(避免线程无限等待),重试机制需加退避策略(如指数退避),避免重试风暴;
    • 缓存优化:热点数据缓存(如 Redis)减少 I/O 次数,降低数据库压力。

7. 性能监控与调优:避免盲目优化

  • 核心风险:无监控情况下盲目优化(如过度使用锁、滥用线程池),可能导致性能不升反降,且难以排查问题。
  • 关键动作:
    • 监控核心指标:CPU 使用率、线程数、锁等待时间、I/O 响应时间、吞吐量、延迟(如通过 Prometheus+Grafana 监控);
    • 定位瓶颈:通过火焰图(如 AsyncProfiler)、线程 dump 分析锁竞争、线程阻塞原因;
    • 按需优化:优先优化瓶颈(如 I/O 瓶颈先做缓存,锁竞争瓶颈先拆锁),避免 “过早优化”(如为了并发而并发,增加代码复杂度)。

8. 业务层面:避免并发场景的业务逻辑漏洞

  • 核心风险:技术层面解决了并发安全,但业务逻辑未考虑并发场景,导致业务异常(如库存超卖、重复下单、幂等性问题)。
  • 解决方案:
    • 幂等设计:接口支持重复调用(如用唯一订单号作为幂等键,避免重复下单);
    • 分布式锁:跨服务 / 跨机器的并发场景(如分布式秒杀),用分布式锁(如 Redis 分布式锁、ZooKeeper 锁)保证数据一致性;
    • 业务限流:针对核心业务(如支付)单独限流,避免非核心业务占用过多资源。

CPU核心数和线程数之间的关系

CPU 核心数是物理硬件资源,线程数(逻辑线程)是操作系统调度的执行单元,线程数通常是核心数的 1-2 倍(超线程技术下),也可通过软件配置远超核心数(如线程池)。

核心概念区分

  • CPU 核心数:指 CPU 芯片上集成的独立运算核心数量(物理硬件),每个核心可独立执行一条指令流,相当于 “工厂里的独立工人”。
  • 线程数(逻辑线程):分为 “硬件线程” 和 “软件线程”。硬件线程是 CPU 通过超线程技术模拟的逻辑执行单元,软件线程是操作系统创建的任务调度单元(如 Java 线程、C++ 线程)。

核心数与线程数的核心关系

  1. 无超线程(HT/SMT)技术:逻辑线程数 = 物理核心数。比如 4 核 CPU,操作系统只能识别 4 个逻辑线程,同一时间最多并行执行 4 个任务。
  2. 有超线程技术:逻辑线程数 = 物理核心数 × 2(主流标准)。比如 4 核 8 线程 CPU,通过硬件层面的指令级并行优化,让一个核心同时处理 2 个线程的指令,操作系统识别为 8 个逻辑线程。
  3. 软件层面的灵活配置:实际编程中,软件线程数可远超核心数(如线程池配置 20 个线程运行在 4 核 CPU 上)。此时操作系统通过 “时间片轮转” 调度,让多个软件线程交替在核心上执行,看似 “并行” 实则 “并发”。

关键补充(避免误解)

  • 超线程不是 “真多核”:一个核心模拟的两个硬件线程,会共享核心的运算资源(如缓存、执行单元),性能提升通常在 30%-50%,而非翻倍。
  • 软件线程数并非越多越好:超过核心数过多时,线程切换(上下文切换)的开销会抵消并发收益,导致系统性能下降。

结合前面聊的 CPU 核心数、线程数关系,以及高并发编程的注意事项,下面直接给 不同 CPU 配置(核心数 / 线程数)对应的线程池配置建议,按「CPU 密集型任务」和「I/O 密集型任务」分类(这是线程池配置的核心区分维度),同时补充配置逻辑和实战细节:

核心配置逻辑(先懂原理再用)

线程池的核心线程数(corePoolSize)和最大线程数(maximumPoolSize),本质是平衡「任务执行效率」和「上下文切换开销」,核心规则:

  1. CPU 密集型任务

    (如数学计算、数据排序、加密解密):任务几乎不阻塞,CPU 一直处于忙碌状态。线程数过多会导致频繁上下文切换,反而拖慢性能。

    配置公式:

    核心线程数 ≈ CPU逻辑线程数(核心数×超线程倍数)±1
    

    (避免 CPU 空闲)。

  2. I/O 密集型任务

    (如数据库查询、网络请求、文件读写):任务大部分时间在等待 I/O 完成(CPU 空闲),线程数可适当多配,让 CPU 在等待期间处理其他任务。

    配置公式:核心线程数 ≈ CPU逻辑线程数 ×(1 + I/O等待时间/任务执行时间) (主流简化版:逻辑线程数 × 2 或 逻辑线程数 × 4,根据 I/O 耗时调整)。

补充:CPU 逻辑线程数 = 物理核心数 × 超线程倍数(无超线程则 ×1,有超线程则 ×2,主流 CPU 如 Intel i5/i7、AMD Ryzen 均支持超线程 / SMT)。

不同 CPU 配置的实战配置表(直接套用)

CPU 配置(物理核心数 / 逻辑线程数)任务类型核心线程数(corePoolSize)最大线程数(maximumPoolSize)适用场景(示例)
2 核 4 线程(如入门级服务器、笔记本)CPU 密集型45-6本地数据计算、轻量算法处理
2 核 4 线程I/O 密集型810-12小型 Web 服务、少量数据库查询
4 核 8 线程(如主流 PC、云服务器 2 核 4G)CPU 密集型89-10中型数据处理、批量计算
4 核 8 线程I/O 密集型1620-24常规 Web 服务、Redis / 数据库操作
8 核 16 线程(如高性能服务器、云服务器 4 核 8G)CPU 密集型1617-18大型计算任务、分布式任务处理
8 核 16 线程I/O 密集型3240-48高并发 Web 服务、秒杀系统、消息消费
16 核 32 线程(如高端服务器、云服务器 8 核 16G)CPU 密集型3233-34超大规模计算、AI 模型训练
16 核 32 线程I/O 密集型6480-96分布式微服务、高吞吐消息队列消费

关键补充说明:

  1. 最大线程数无需远超核心线程数:I/O 密集型任务中,最大线程数比核心线程数多 20%-50% 即可,避免突发流量时创建过多线程导致资源耗尽;
  2. 空闲线程存活时间(keepAliveTime):I/O 密集型建议设为 30-60 秒(减少线程频繁创建销毁),CPU 密集型可设为 10-20 秒(空闲线程快速回收);
  3. 任务队列(workQueue):CPU 密集型用小队列(如ArrayBlockingQueue(100)),避免任务堆积;I/O 密集型用中等队列(如LinkedBlockingQueue(1000)),缓冲突发流量(但需配合限流,避免队列溢出)。

不同语言的线程池配置示例(落地代码)

1. Java(ThreadPoolExecutor)

以「4 核 8 线程 CPU + I/O 密集型任务」为例:

import java.util.concurrent.*;

public class ThreadPoolConfig {
    public static void main(String[] args) {
        // CPU逻辑线程数(4核8线程)
        int cpuLogicalThreads = Runtime.getRuntime().availableProcessors(); // 输出8
        // 核心线程数=8×2=16,最大线程数=20,队列容量1000,空闲线程存活30秒
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
            16, // corePoolSize
            20, // maximumPoolSize
            30, // keepAliveTime(秒)
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(1000), // 任务队列
            new ThreadPoolExecutor.CallerRunsPolicy() // 队列满时,调用线程执行(避免任务丢失)
        );
        
        // 提交任务(示例)
        for (int i = 0; i < 1000; i++) {
            threadPool.submit(() -> {
                // 模拟I/O任务(如数据库查询)
                try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
                System.out.println("任务执行完成");
            });
        }
        
        threadPool.shutdown();
    }
}

2. Go(goroutine + worker pool)

Go 无需手动创建线程池(runtime 自动管理 M-P-G 模型),但高并发场景需控制 goroutine 数量(避免过多导致调度开销),以「8 核 16 线程 CPU + I/O 密集型任务」为例:

package main

import (
	"fmt"
	"time"
)

func main() {
	// CPU逻辑线程数(8核16线程)
	cpuLogicalThreads := 16
	// 控制goroutine数量(I/O密集型=16×2=32)
	workerPool := make(chan struct{}, 32)
	taskCount := 2000

	for i := 0; i < taskCount; i++ {
		workerPool <- struct{}{} // 占用一个worker名额
		go func(taskId int) {
			defer func() { <-workerPool }() // 释放worker名额
			// 模拟I/O任务(如HTTP请求)
			time.Sleep(100 * time.Millisecond)
			fmt.Printf("任务%d执行完成\n", taskId)
		}(i)
	}

	// 等待所有任务完成(简化写法,实际用sync.WaitGroup)
	time.Sleep(5 * time.Second)
	close(workerPool)
}

3. Python(ThreadPoolExecutor)

Python 受 GIL(全局解释器锁)影响,CPU 密集型任务建议用ProcessPoolExecutor(多进程),I/O 密集型用ThreadPoolExecutor,以「4 核 8 线程 CPU + I/O 密集型任务」为例:

from concurrent.futures import ThreadPoolExecutor
import time

def io_task(task_id):
    # 模拟I/O任务(如网络请求)
    time.sleep(0.1)
    print(f"任务{task_id}执行完成")

if __name__ == "__main__":
    cpu_logical_threads = 8  # 4核8线程
    core_threads = 16  # I/O密集型=8×2
    max_threads = 20

    # Python的ThreadPoolExecutor无maximumPoolSize,核心线程数直接设为16
    with ThreadPoolExecutor(max_workers=16) as executor:
        # 提交1000个任务
        for i in range(1000):
            executor.submit(io_task, i)

调优技巧(避免踩坑)

  1. 先看监控再调优:通过工具查看 CPU 使用率、线程等待时间、任务队列长度(如 Java 用 JVisualVM、Go 用 pprof),如果 CPU 使用率长期低于 70%(I/O 密集型),可适当增加线程数;如果 CPU 使用率 100% 且上下文切换频繁(如 Java 用vmstat看cs列),需减少线程数;
  2. 避免 “一刀切”:不同业务模块用独立线程池(如支付模块、商品模块分开配置),避免单个业务占用过多线程;
  3. 结合限流熔断:线程池配置配合限流(如令牌桶算法),避免突发流量导致任务队列溢出或线程数暴增;
  4. 超线程的影响:如果 CPU 支持超线程,但任务是 CPU 密集型,建议线程数 = 物理核心数(而非逻辑线程数),因为超线程对 CPU 密集型任务提升有限,过多线程会增加调度开销。

总结

线程池配置的核心是「匹配 CPU 资源和任务类型」:

  • CPU 密集型:线程数≈CPU 逻辑线程数(±1),避免上下文切换;

  • I/O 密集型:线程数≈CPU 逻辑线程数 ×2~4,充分利用 CPU 空闲时间;

    实际使用中,先按上述建议配置,再根据监控数据微调(如 I/O 耗时特别长,可适当增加线程数;任务队列频繁满,可扩大队列或增加最大线程数)。

CPU时间片轮转机制

CPU 时间片轮转是操作系统的抢占式调度算法,把 CPU 时间分割成固定长度的 “时间片”,让多个线程 / 进程轮流占用 CPU,从而实现 “并发执行” 的效果。

核心原理

操作系统把 CPU 的总运行时间,切分成一个个短时间片(比如 10-100 毫秒),给就绪队列里的每个线程 / 进程分配一个时间片。线程在时间片内执行任务,时间片用完后,操作系统会暂停该线程,保存其执行状态(上下文切换),再从就绪队列中选下一个线程分配时间片,循环往复。

关键细节(理解机制的核心)

  1. 时间片的长度:不是固定值,由操作系统动态调整(如 Linux 根据系统负载调整),太短会导致上下文切换频繁(开销大),太长会导致并发响应变慢(如一个线程占 CPU1 秒,其他线程需等待)。
  2. 上下文切换:时间片切换时,操作系统会保存当前线程的寄存器状态、程序计数器等信息,再加载下一个线程的状态,这个过程有性能开销(占 CPU 时间的 1%-10%)。
  3. 就绪队列:所有等待 CPU 的线程 / 进程会进入就绪队列,操作系统按 “先进先出” 或优先级策略,选择下一个要执行的线程。
  4. 抢占式特性:即使线程没执行完任务,时间片用完也会被强制暂停,避免单个线程长时间占用 CPU。

核心作用(为什么需要这个机制)

  • 让单核心 CPU 能 “同时” 运行多个线程 / 进程,比如你电脑同时开浏览器、编辑器、音乐软件,本质是 CPU 在这些进程间快速切换。
  • 保证并发公平性:每个线程都能分到 CPU 时间,不会出现某个线程一直抢占资源的情况。
  • 提升系统响应速度:短时间片让交互类任务(如点击鼠标、输入文字)能快速获得 CPU 响应,感觉 “无卡顿”。

和高并发编程的关联(避坑关键)

  • 线程数并非越多越好:过多线程会导致时间片切换频繁,上下文切换开销抵消并发收益(比如 4 核 CPU 开 1000 个线程,大部分 CPU 时间都用来切换,而非执行任务)。
  • 时间片对任务类型的影响:CPU 密集型任务(如计算)怕频繁切换,需控制线程数接近 CPU 核心数;I/O 密集型任务(如等待网络 / 数据库)在时间片内常处于等待状态,可适当多开线程,利用等待时间让其他线程执行。

时间片轮转机制下的线程数优化实战

核心目标:在时间片轮转调度规则下,平衡 “并发吞吐量” 与 “上下文切换开销”—— 既不让 CPU 空闲,也不因线程过多导致切换开销吞噬性能。优化的核心逻辑是:线程数必须匹配任务类型(CPU 密集 / I/O 密集)和 CPU 硬件配置(核心数 / 逻辑线程数),避免 “线程过多” 或 “线程不足” 的极端情况。

先明确 3 个核心前提(优化的基础)

在动手配置前,必须先理清以下关键信息,否则优化会盲目:

1. 确定 CPU 核心配置(关键参数)

  • 物理核心数(CPU Physical Cores):通过lscpu(Linux)、taskmgr(Windows)、Runtime.getRuntime().availableProcessors()(Java)获取;
  • 逻辑线程数(CPU Logical Threads):物理核心数 × 超线程倍数(无超线程 = 1,有超线程 = 2,主流 CPU 默认开启);
  • 示例:4 核 8 线程 CPU(逻辑线程数 = 8)、8 核 16 线程 CPU(逻辑线程数 = 16)。

2. 区分任务类型(核心决策依据)

任务类型核心特征时间片利用情况优化目标
CPU 密集型任务几乎无阻塞(如计算、排序、加密),CPU 持续高负载(使用率 80%+)时间片内 CPU 一直忙碌,切换会浪费时间减少切换,让 CPU 专注执行任务
I/O 密集型任务大部分时间在等待 I/O(数据库查询、网络请求、文件读写),CPU 空闲率高时间片内大量时间等待,CPU 闲置多开线程,利用空闲 CPU 处理更多任务

3. 理解时间片轮转的约束

  • 时间片长度:主流 OS(Linux/Windows)默认 10-100ms,太短会增加切换开销,太长会降低并发响应;
  • 上下文切换成本:每次切换约消耗 1-10μs,线程数越多,切换越频繁(如 1000 个线程切换开销可能占 CPU 的 30%+);
  • 核心约束:线程数超过 “逻辑线程数 × 任务阻塞系数” 后,性能会呈下降趋势(阻塞系数:I/O 密集型 > 1,CPU 密集型≈1)。

通用线程数计算公式(直接套用)

基于时间片轮转机制和任务类型,提炼出 2 类任务的通用公式,结合 CPU 配置直接计算:

1. CPU 密集型任务(无阻塞 / 低阻塞)

  • 公式:最优线程数 = CPU逻辑线程数 ± 1
  • 逻辑:CPU 几乎无空闲,线程数过多会导致频繁切换,过少会导致 CPU 空闲;±1 是为了避免 CPU 核心 “idle”(空闲)。
  • 特殊情况:如果 CPU 支持超线程,但任务是纯计算(如矩阵乘法),超线程提升有限,可调整为物理核心数 ± 1(避免逻辑线程竞争同一核心的运算资源)。

2. I/O 密集型任务(高阻塞)

  • 基础公式:最优线程数 = CPU逻辑线程数 ×(1 + I/O等待时间 / 任务执行时间)
  • 简化公式(无需精准统计时间):最优线程数 = CPU逻辑线程数 × 2 ~ 4(I/O 等待越久,乘数越大,最大不超过 8)
  • 逻辑:I/O 等待期间,CPU 可切换到其他线程执行,线程数需覆盖 “等待时间窗口”,让 CPU 始终有任务可做;但乘数超过 8 后,切换开销会超过并发收益。
  • 精准计算示例:某任务执行时间(CPU 工作)=10ms,I/O 等待时间 = 30ms,则1+30/10=4,最优线程数 = 逻辑线程数 ×4。

3. 混合任务(部分 CPU 密集 + 部分 I/O 密集)

  • 公式:最优线程数 = CPU逻辑线程数 ×(1 + 平均I/O等待时间 / 平均任务执行时间)
  • 逻辑:先统计任务中 “CPU 工作时间” 和 “I/O 等待时间” 的平均值,再代入公式;如果无法统计,按 “I/O 密集型” 取中间值(逻辑线程数 ×2.5)。

不同 CPU 配置的实战配置表(落地即用)

结合主流 CPU 配置,按任务类型给出具体线程数(核心线程数 + 最大线程数),适配线程池(如 Java ThreadPoolExecutor、Go Worker Pool):

CPU 配置(物理核 / 逻辑线程)任务类型核心线程数(corePoolSize)最大线程数(maximumPoolSize)空闲线程存活时间(keepAliveTime)任务队列容量
2 核 4 线程(入门服务器 / 笔记本)CPU 密集型45-610-20 秒(快速回收空闲线程)100-200(小队列)
2 核 4 线程I/O 密集型8-1212-1630-60 秒(减少创建销毁开销)500-1000(中等队列)
4 核 8 线程(主流 PC / 云服务器 2 核 4G)CPU 密集型89-1010-20 秒200-300
4 核 8 线程I/O 密集型16-2424-3230-60 秒1000-2000
8 核 16 线程(高性能服务器 / 4 核 8G)CPU 密集型1617-1810-20 秒300-500
8 核 16 线程I/O 密集型32-4848-6460 秒2000-5000
16 核 32 线程(高端服务器 / 8 核 16G)CPU 密集型3233-3410-20 秒500-1000
16 核 32 线程I/O 密集型64-9696-12860 秒5000-10000

配置说明:

  1. 最大线程数:比核心线程数多 20%-50%,用于应对突发流量(如 I/O 密集型任务中,部分线程因 I/O 阻塞,需额外线程处理新请求);
  2. 任务队列:CPU 密集型用小队列(避免任务堆积导致延迟),I/O 密集型用中等队列(缓冲突发流量,但需配合限流,避免队列溢出);
  3. 超线程适配:如果关闭超线程(如部分数据库服务器),逻辑线程数 = 物理核心数,公式和配置按 “物理核心数” 重新计算。

不同语言的线程数配置实战示例

1. Java(ThreadPoolExecutor)

以 “4 核 8 线程 CPU + I/O 密集型任务” 为例(对应上表配置):

import java.util.concurrent.*;

public class OptimalThreadPool {
    public static void main(String[] args) {
        // 1. 获取CPU逻辑线程数(4核8线程→输出8)
        int cpuLogicalThreads = Runtime.getRuntime().availableProcessors();
        
        // 2. 按I/O密集型公式计算:核心线程数=8×2=16,最大线程数=24,队列容量1000
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
            16,                      // corePoolSize(核心线程数)
            24,                      // maximumPoolSize(最大线程数)
            30,                      // keepAliveTime(30秒)
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(1000),  // 任务队列
            new ThreadPoolExecutor.CallerRunsPolicy()  // 队列满时,调用线程执行(避免任务丢失)
        );
        
        // 3. 提交任务(模拟数据库查询I/O)
        for (int i = 0; i < 2000; i++) {
            int taskId = i;
            threadPool.submit(() -> {
                try {
                    // 模拟I/O等待(数据库查询耗时50ms)
                    Thread.sleep(50);
                    System.out.println("任务" + taskId + "执行完成,线程:" + Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }
        
        threadPool.shutdown();
    }
}

2. Go(Goroutine Worker Pool)

Go 无手动线程池,需控制 Goroutine 数量(避免调度开销),以 “8 核 16 线程 CPU + I/O 密集型任务” 为例:

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	// 1. CPU逻辑线程数(8核16线程)
	cpuLogicalThreads := 16
	// 2. I/O密集型:Goroutine数量=16×2=32(控制并发数)
	workerCount := 32
	taskCount := 5000

	// 3. 用通道控制Worker Pool
	workerChan := make(chan struct{}, workerCount)
	var wg sync.WaitGroup

	for i := 0; i < taskCount; i++ {
		wg.Add(1)
		workerChan <- struct{}{} // 占用一个Worker名额(控制并发数)

		go func(taskId int) {
			defer func() {
				<-workerChan // 释放Worker名额
				wg.Done()
			}()

			// 模拟I/O任务(网络请求耗时100ms)
			time.Sleep(100 * time.Millisecond)
			fmt.Printf("任务%d执行完成,GoroutineID:%d\n", taskId, getGoroutineID())
		}(i)
	}

	wg.Wait()
	close(workerChan)
}

// 辅助函数:获取Goroutine ID(仅用于示例)
func getGoroutineID() uint64 {
	b := make([]byte, 64)
	b = b[:runtime.Stack(b, false)]
	b = bytes.TrimPrefix(b, []byte("goroutine "))
	b = b[:bytes.IndexByte(b, ' ')]
	n, _ := strconv.ParseUint(string(b), 10, 64)
	return n
}

3. Python(ThreadPoolExecutor/ProcessPoolExecutor)

Python 受 GIL 限制,CPU 密集型需用多进程(ProcessPoolExecutor),I/O 密集型用多线程(ThreadPoolExecutor),以 “2 核 4 线程 CPU + CPU 密集型任务” 为例:

from concurrent.futures import ProcessPoolExecutor
import time

# 模拟CPU密集型任务(计算)
def cpu_task(task_id):
    result = 0
    for i in range(10**6):
        result += i
    print(f"CPU任务{task_id}执行完成,结果:{result}")
    return result

if __name__ == "__main__":
    # 1. CPU逻辑线程数(2核4线程→4)
    cpu_logical_threads = 4
    # 2. CPU密集型:进程数=4(避免GIL限制)
    with ProcessPoolExecutor(max_workers=4) as executor:
        # 提交10个CPU密集型任务
        futures = [executor.submit(cpu_task, i) for i in range(10)]
        # 等待所有任务完成
        for future in futures:
            future.result()

优化验证与调优技巧(避免踩坑)

配置完线程数后,必须通过监控验证效果,再动态微调,核心步骤如下:

1. 核心监控指标(必看)

指标工具(Linux)优化判断标准
CPU 使用率top、mpstatCPU 密集型:稳定在 70%-80%(过高 = 线程过多 / 任务过重,过低 = 线程不足);I/O 密集型:稳定在 30%-60%(过高 = 切换频繁)
上下文切换次数(cs)vmstat、pidstat正常范围:每秒数千 - 数万;如果超过 10 万 / 秒,且 CPU 使用率高,说明线程过多,需减少
线程等待时间jstack(Java)、pprof(Go)大量线程处于 WAITING 状态(如 Java 的 WAITING on Condition),说明线程过多,需减少;大量线程处于 RUNNABLE 状态,说明线程不足
任务队列长度自定义监控(线程池)队列长期满溢,说明最大线程数或队列容量不足;队列长期为空,说明核心线程数过多

2. 调优步骤(迭代优化)

  1. 初始配置:按上述公式 / 表格设置线程数;
  2. 压测验证:用 JMeter、Locust 等工具压测(模拟目标并发量);
  3. 指标观察:查看 CPU 使用率、上下文切换、队列长度;
  4. 动态微调:
    • 如果 CPU 使用率 < 50% 且任务响应慢→线程不足,增加核心线程数(I/O 密集型可翻倍);
    • 如果上下文切换 > 10 万 / 秒且 CPU 使用率高→线程过多,减少最大线程数(每次减 20%);
    • 如果队列频繁满溢→先扩大队列容量(不超过 1 万),再增加最大线程数;
    • 如果任务有峰值流量→保留最大线程数的弹性(核心线程数按平时流量配置,最大线程数按峰值配置)。

3. 避坑指南

  • 坑 1:线程数越多越好→× 超过 “逻辑线程数 × 阻塞系数” 后,切换开销会抵消并发收益;
  • 坑 2:所有业务共用一个线程池→× 不同任务类型(CPU/I/O)分开配置线程池,避免互相影响(如支付模块用独立线程池);
  • 坑 3:忽略系统限制→× 调整线程池前,需提升系统文件描述符上限(ulimit -n 65535),避免 “too many open files” 报错;
  • 坑 4:超线程下 CPU 密集型按逻辑线程数配置→× 纯计算任务可按 “物理核心数” 配置,避免逻辑线程竞争同一核心资源。

总结

时间片轮转机制下,线程数优化的核心是 “匹配 CPU 资源与任务特性”:

  • CPU 密集型:线程数≈CPU 逻辑线程数(±1),减少上下文切换;
  • I/O 密集型:线程数≈CPU 逻辑线程数 ×2~4,利用 CPU 空闲时间;
  • 实战中:先按公式配置,再通过压测和监控微调,最终达到 “CPU 不空闲、切换开销低、任务响应快” 的平衡。

其它具体场景(如秒杀系统、大数据计算、实时消息推送)更精准的线程数优化。

C10K 问题

本质、解决方案与现代演进

C10K 是 “Concurrent 10,000 Connections” 的缩写,核心是 “单台服务器如何高效处理 1 万个并发 TCP 连接”—— 这是互联网早期(2000 年左右)提出的经典技术挑战,本质是解决 “有限硬件资源” 与 “海量并发连接” 的矛盾,也是高并发编程的核心场景之一。

C10K 问题的本质:为什么早期服务器扛不住 1 万并发?

早期服务器(2000 年前后)普遍用 “一个连接对应一个线程 / 进程” 的模型(如 Apache 的 prefork 模式),这种模型在并发达到 1 万时会彻底崩溃,核心原因有 3 点:

  1. 资源耗尽:每个线程 / 进程会占用独立的栈内存(通常几 MB 到几十 MB),1 万个线程仅栈内存就需要几十 GB(如 10K 线程 ×4MB 栈 = 40GB),远超服务器物理内存,直接导致 OOM(内存溢出);
  2. 上下文切换开销爆炸:CPU 时间片轮转机制下,1 万个线程会频繁切换,上下文切换的开销(保存 / 加载线程状态)会占满 CPU 时间,导致真正用于执行业务逻辑的 CPU 占比极低;
  3. 文件描述符限制:Linux 系统中,每个 TCP 连接对应一个文件描述符(fd),早期系统默认限制单进程最大文件描述符为 1024,未调整前根本无法创建 1 万个连接。

解决 C10K 的核心思路:从 “多线程” 到 “I/O 多路复用”

解决 C10K 的关键是 “用少量线程管理大量连接”,核心技术是 I/O 多路复用—— 让一个线程能同时监听多个 TCP 连接的 I/O 事件(如 “连接建立”“数据可读”“数据可写”),只有当连接有实际 I/O 操作时才分配 CPU 资源处理,避免线程空等。

1. 三大经典 I/O 多路复用技术(跨平台核心方案)

技术适用系统核心原理优缺点
select跨平台(Linux/Windows)用数组存储文件描述符,线程轮询检查哪些 fd 有 I/O 事件优点:跨平台;缺点:fd 上限低(默认 1024)、轮询效率低(遍历整个数组)
pollLinux/Unix用链表存储文件描述符,解决 select 的 fd 上限问题优点:无 fd 数量限制;缺点:仍需轮询遍历,高并发下效率下降
epollLinux(2.6 内核后)基于 “事件驱动”+“红黑树”,仅通知有 I/O 事件的 fd,无需轮询优点:fd 无上限、事件驱动效率高(O (1) 复杂度);缺点:仅支持 Linux,无跨平台

核心结论:Linux 环境下 epoll 是解决 C10K 的最优方案(目前主流服务器如 Nginx、Redis 均基于 epoll 实现),Windows 用 IOCP、FreeBSD 用 kqueue,本质都是 “事件驱动 + I/O 多路复用”。

2. 配套优化:突破系统限制

除了 I/O 模型改造,还需调整系统参数突破默认限制:

  • 提升文件描述符上限:ulimit -n 65535(临时)或修改/etc/security/limits.conf(永久),让单进程可创建数万 fd;
  • 调整 TCP 参数:如开启tcp_tw_reuse(复用 TIME_WAIT 状态的连接)、tcp_tw_recycle(快速回收 TIME_WAIT 连接),减少端口占用;
  • 内存优化:使用 “线程池” 而非 “一个连接一个线程”,控制线程数在 CPU 核心数的 2-4 倍(I/O 密集型),避免内存溢出。

C10K 的经典解决方案架构

现代服务器解决 C10K 的架构已形成标准范式,核心是 “I/O 多路复用 + 事件驱动 + 线程池”,以 Nginx 为例:

  1. Master 进程:管理配置、监听端口、启动 Worker 进程;
  2. Worker 进程:数量通常等于 CPU 核心数(如 4 核 8 线程设 8 个 Worker),每个 Worker 进程基于 epoll 监听多个连接;
  3. 事件驱动:Worker 进程通过 epoll 等待 I/O 事件(如客户端连接、数据到达),有事件时才处理,无事件时阻塞等待(不占用 CPU);
  4. 非阻塞 I/O:所有 I/O 操作(如读数据、写数据)设为非阻塞,避免单个连接的 I/O 等待阻塞整个进程;
  5. 线程池(可选):复杂业务逻辑(如数据库查询)交给线程池处理,避免阻塞 Worker 进程的 I/O 事件循环。

架构优势:单个 Worker 进程可处理数万并发连接,多个 Worker 进程充分利用多核 CPU,整体能轻松支撑 10 万 + 并发(远超 C10K)。

C10K 的演进:从 C10K 到 C10M/C100M

随着硬件(多核 CPU、大内存)和软件技术的发展,C10K 已不是瓶颈,行业已迈向 C10M(1000 万并发) 甚至 C100M(1 亿并发),核心优化方向:

  1. 内核优化:禁用不必要的内核功能(如关闭 TCP Timestamp)、优化 epoll 触发模式(边缘触发 ET 比水平触发 LT 更高效);
  2. 用户态协议栈:绕过 Linux 内核协议栈,使用 DPDK(数据平面开发套件)、XDP(eXpress Data Path)等技术,在用户态直接处理网络包,减少内核态与用户态的切换开销;
  3. 无锁编程:避免多线程竞争锁,使用无锁数据结构(如环形缓冲区)提升并发效率;
  4. 硬件加速:利用网卡的 RSS(接收端缩放)功能,将不同连接的数据包分发到不同 CPU 核心,避免单核心瓶颈。

和高并发编程的关联(实战避坑)

  1. 避免 “一个连接一个线程”:Java 中不要用BIO(阻塞I/O)+ 线程池处理高并发连接,应使用NIO(非阻塞I/O)+ Selector(对应 epoll)或 Netty 框架(封装了 epoll + 事件驱动);
  2. 合理设置文件描述符上限:部署高并发服务时,必须调整系统 fd 限制(如ulimit -n 65535),否则会因 “too many open files” 报错;
  3. 选择合适的 I/O 模型:Linux 环境优先用 epoll(Java NIO 的 Selector 在 Linux 下底层就是 epoll),Windows 用 IOCP,避免用 select(fd 上限低、效率差);
  4. 控制线程数:即使是 I/O 密集型任务,线程数也无需超过 CPU 核心数 ×4,过多线程会导致上下文切换开销激增,抵消并发收益。

总结

C10K 问题的核心是 “突破‘连接 - 线程’绑定的资源限制”,解决方案的本质是 “用 I/O 多路复用让少量线程管理大量连接”。现代高并发框架(如 Netty、Nginx、Redis)都已内置这些优化,开发者无需重复造轮子,但需理解底层原理:避免阻塞 I/O、合理调整系统参数、利用事件驱动模型,才能应对万级以上的并发连接。

其它高并发场景问题

除了经典的 C10K(万级并发连接),高并发领域还有一系列以 “并发规模”“场景特性”“资源瓶颈” 为核心的衍生问题,本质都是 “有限资源应对海量请求 / 数据” 的矛盾,但聚焦的瓶颈点(连接、流量、数据、延迟等)不同。以下是最核心、最常见的类似高并发问题,按 “规模演进” 和 “场景细分” 分类,结合本质、挑战与解决方案:

按 “并发规模演进” 的核心问题(从 C10K 到 C100M)

这类问题是 C10K 的直接延伸,核心挑战是 “连接数 / 请求数指数级增长”,突破单机 / 集群的资源上限:

1. C10M 问题(1000 万并发连接)

  • 定义:单台服务器支撑 1000 万并发 TCP 连接(Concurrent 10 Million Connections),是 C10K 的 1000 倍。
  • 核心瓶颈:
    • 内核资源耗尽:Linux 每个 TCP 连接占用内核内存(如 socket 缓冲区、TCP 控制块),1000 万连接需消耗数十 GB 内核内存;
    • 网络中断处理瓶颈:单网卡接收数据包的速率有限,传统内核协议栈处理能力不足;
    • 并发调度开销:即使是事件驱动模型,海量连接的事件分发、状态管理也会带来开销。
  • 解决方案:
    • 内核优化:关闭不必要的 TCP 特性(如 Timestamp、TCP 缓存)、调整 socket 缓冲区大小(net.core.somaxconn);
    • 用户态协议栈:用 DPDK(数据平面开发套件)、XDP 绕过内核,直接在用户态处理网络包,减少内核态 / 用户态切换;
    • 硬件优化:网卡开启 RSS(接收端缩放),将数据包分发到多个 CPU 核心;使用大页内存(HugePage)减少内存寻址开销;
    • 无锁设计:用无锁环形缓冲区、原子操作替代锁,避免多线程竞争开销。
  • 典型场景:高性能网关、实时消息推送服务(如直播弹幕、IM 长连接)。

2. C100M 问题(1 亿并发连接)

  • 定义:单台服务器支撑 1 亿并发 TCP 连接(Concurrent 100 Million Connections),是极致的单机并发极限。
  • 核心瓶颈:
    • 内存天花板:即使每个连接仅占用 1KB 内核内存,1 亿连接也需 100GB 内存,对硬件要求极高;
    • 网卡带宽与中断:1 亿连接的数据包收发需万兆 / 40G 网卡,且中断处理需完全卸载到硬件(如网卡 RSS 分流到 32+ CPU 核心);
    • 协议栈效率:传统 TCP 协议在海量连接下的状态管理(如拥塞控制、超时重传)开销过大。
  • 解决方案:
    • 专用硬件:使用高性能服务器(多 CPU 插槽、TB 级内存、40G/100G 网卡);
    • 轻量级协议:用 UDP 替代 TCP(如 QUIC 协议),减少连接状态管理开销;
    • 连接复用:通过长连接池、代理转发(如 Nginx 反向代理)合并海量客户端连接,减少后端服务器的直接连接数;
    • 分布式拆分:单台服务器难以承载时,用 “集群 + 负载均衡” 拆分连接(如 IM 服务按用户 ID 哈希分片)。
  • 典型场景:大型云厂商的网关服务、全球分布式 IM 平台(如微信、WhatsApp)。

3. C100K 问题(10 万并发请求)

  • 定义:单台服务器每秒处理 10 万并发请求(Queries Per Second,QPS),而非 “连接数”—— 核心是 “请求吞吐量” 而非 “连接持有”。
  • 核心瓶颈:
    • CPU 密集型瓶颈:请求处理逻辑复杂(如复杂计算、数据库聚合查询),CPU 算力不足;
    • I/O 密集型瓶颈:数据库、Redis 等存储的读写吞吐量跟不上,成为瓶颈;
    • 线程 / 协程调度:过多请求导致线程 / 协程切换频繁,上下文开销抵消并发收益。
  • 解决方案:
    • 计算层优化:用协程(Go goroutine、Java Virtual Threads)替代线程,减少调度开销;核心逻辑并行化(如拆分任务到多核);
    • 存储层优化:热点数据缓存(Redis 集群)、数据库分库分表(如 ShardingSphere)、读写分离;
    • 限流降级:通过令牌桶 / 漏桶算法限制 QPS,避免超出系统承载能力;
    • 无状态设计:服务端无本地状态,支持横向扩容(增加机器即可提升 QPS)。
  • 典型场景:电商商品详情页、短视频列表页、API 网关。

按 “场景细分” 的高并发问题(聚焦特定瓶颈)

这类问题不局限于 “连接数 / 请求数”,而是聚焦高并发下的 “数据一致性”“延迟”“峰值冲击” 等特定挑战,是实际业务中更常遇到的场景:

1. C10K/C100K 写并发(写密集型高并发)

  • 定义:大量请求同时写入数据(如秒杀下单扣库存、直播点赞、用户签到),核心挑战是 “数据一致性” 与 “写吞吐量”。
  • 核心瓶颈:
    • 并发冲突:多请求同时修改同一数据(如库存、余额),导致超卖、少扣、数据不一致;
    • 存储写瓶颈:数据库单表写入性能有限(MySQL 单表每秒写约 1-10 万),海量写请求堆积;
    • 锁竞争:分布式场景下,分布式锁争抢导致性能下降或死锁。
  • 解决方案:
    • 数据分片:按用户 ID / 商品 ID 分库分表,分散写压力(如秒杀商品单独分表);
    • 异步化写入:用消息队列(Kafka、RabbitMQ)缓冲写请求,后端异步消费写入数据库,削峰填谷;
    • 乐观锁 / 版本号:避免悲观锁的阻塞,用 CAS 或版本号控制(如 MySQL WHERE id=? AND version=?);
    • 分布式锁:用 Redis/ZooKeeper 分布式锁保证跨服务的数据一致性(如秒杀库存扣减);
    • 最终一致性:非核心场景(如点赞数)采用最终一致性,先写入缓存,再异步同步到数据库。
  • 典型场景:电商秒杀、直播带货下单、节日活动签到。

2. 热点数据高并发访问(读密集型高并发)

  • 定义:少量 “热点数据” 被海量请求同时读取(如爆款商品详情、热门短视频、明星直播间信息),核心挑战是 “读吞吐量” 与 “缓存有效性”。
  • 核心瓶颈:
    • 缓存穿透 / 击穿 / 雪崩:缓存未命中导致请求穿透到数据库;热点 key 过期导致大量请求击穿到数据库;缓存集群宕机导致雪崩;
    • 缓存一致性:数据更新后,缓存未及时更新,导致读旧数据;
    • 网络带宽:热点数据的高频传输占用大量网卡带宽。
  • 解决方案:
    • 多级缓存:本地缓存(如 Caffeine、Guava Cache)+ 分布式缓存(Redis 集群),减少分布式缓存访问压力;
    • 热点 key 优化:将热点 key 分散到多个缓存节点(如给 key 加随机前缀),避免单节点瓶颈;热点 key 永不过期(或异步更新);
    • 缓存防护:用布隆过滤器防止缓存穿透;缓存击穿时用互斥锁限制数据库访问;缓存雪崩时设置过期时间随机化、集群熔断;
    • 静态化:将热点数据静态化(如生成 HTML 页面、CDN 分发),直接由 CDN 响应,无需回源。
  • 典型场景:双 11 爆款商品页、热门短视频播放、体育赛事直播弹幕。

3. 高并发下的延迟敏感问题(低延迟高并发)

  • 定义:海量请求不仅要求高吞吐量,还要求极低响应延迟(如毫秒级、微秒级),核心挑战是 “减少等待与开销”。
  • 核心瓶颈:
    • I/O 延迟:数据库、Redis、网络请求的延迟累积(如一次请求需 3 次数据库查询,每次 100ms,总延迟 300ms);
    • 上下文切换:线程 / 进程切换、内核态 / 用户态切换带来的开销;
    • 锁等待:并发竞争导致线程阻塞等待锁。
  • 解决方案:
    • 异步非阻塞 I/O:用 Netty(Java)、Golang 协程 + channel 等异步框架,减少 I/O 等待;
    • 零拷贝技术:用 mmap、sendfile 减少数据拷贝(如 Nginx 静态文件传输、Kafka 消息收发);
    • 无锁编程:用 CAS、无锁数据结构(如 ConcurrentLinkedQueue)替代锁,避免阻塞;
    • 本地化部署:将计算、存储部署在同一机房 / 同一节点,减少网络延迟;
    • 硬件优化:使用 SSD 替代 HDD 减少存储延迟,使用大内存减少磁盘 I/O。
  • 典型场景:高频交易系统(股票、数字货币)、实时风控、自动驾驶数据传输。

4. 分布式高并发问题(跨节点高并发)

  • 定义:高并发请求分散在多台服务器 / 多地域,核心挑战是 “分布式协调”“数据一致性”“负载均衡”。
  • 核心瓶颈:
    • 分布式事务:跨服务 / 跨数据库的事务一致性(如下单 = 扣库存 + 减余额 + 生成订单);
    • 负载均衡不均:请求集中在部分节点,导致 “忙的忙死,闲的闲死”;
    • 网络分区:跨地域 / 跨机房的网络延迟、抖动,导致服务不可用。
  • 解决方案:
    • 分布式事务:用 2PC/3PC(强一致性)、TCC、SAGA 模式(最终一致性)、本地消息表 + 消息队列(最终一致性);
    • 负载均衡优化:用一致性哈希(避免节点扩容 / 缩容导致缓存失效)、智能负载均衡(如根据节点 CPU / 内存使用率分发请求);
    • 服务网格(Istio):统一管理服务发现、负载均衡、熔断降级,减少分布式协调复杂度;
    • 多活部署:同城双活、异地多活,避免单地域故障,同时降低跨地域延迟。
  • 典型场景:分布式电商平台、跨地域支付系统、全球 CDN 服务。

5. 峰值流量冲击问题(突发高并发)

  • 定义:短时间内出现远超平时的突发流量(如演唱会门票开售、秒杀活动启动、热点事件爆发),核心挑战是 “削峰填谷” 与 “系统稳定性”。
  • 核心瓶颈:
    • 资源耗尽:突发流量导致 CPU、内存、网络带宽瞬间占满,服务宕机;
    • 队列溢出:消息队列、任务队列因突发流量堆积溢出;
    • 级联故障:一个服务崩溃导致依赖它的服务连锁崩溃。
  • 解决方案:
    • 限流熔断:用 Sentinel、Hystrix 等工具限流(限制每秒请求数)、熔断(服务故障时快速返回降级结果);
    • 削峰填谷:用消息队列缓冲突发流量,后端按能力消费(如秒杀请求先写入 Kafka,再由消费者异步处理下单);
    • 弹性扩容:用云原生技术(K8s HPA)根据流量自动扩容实例,流量峰值过后自动缩容;
    • 降级兜底:核心服务优先保障(如支付),非核心服务降级(如商品评论、推荐系统暂时关闭);
    • 流量控制:前端限流(如按钮置灰、验证码)、后端排队(如秒杀请求排队处理,超出队列长度直接返回 “活动火爆”)。
  • 典型场景:演唱会门票开售、电商秒杀、热点事件引发的流量爆发(如明星官宣结婚)。

核心总结:所有高并发问题的本质与通用解法

无论是 C10K/C10M 这类 “规模型” 问题,还是热点数据、峰值冲击这类 “场景型” 问题,本质都是 “资源(CPU、内存、网络、存储)的供给无法匹配需求”,通用解法可归纳为 4 类:

  1. 减少资源消耗:用异步非阻塞、无锁编程、零拷贝减少 CPU / 内存 / 网络开销;
  2. 提高资源利用率:用多核并行、缓存、池化(线程池、连接池)充分利用硬件资源;
  3. 分散压力:用分布式拆分、数据分片、负载均衡将压力分散到多节点;
  4. 控制需求:用限流、降级、排队机制控制请求量,避免超出系统承载能力。

实际业务中,高并发问题往往是 “复合型” 的(如秒杀同时涉及 “峰值冲击”“写并发”“热点数据”),需结合场景组合使用上述解法(如秒杀 = 限流 + 消息队列削峰 + 分布式锁 + 缓存 + 分库分表)。

其它场景(如分布式事务、缓存雪崩、秒杀架构)

最近更新:: 2025/11/27 22:07
Contributors: luokaiwen