并发编程一
并发编程核心知识点笔记
一、基础概念:并发 vs 并行
这是并发编程最容易混淆的两个概念,也是理解一切的起点:
- 并行:同一时刻,多个线程/进程在不同CPU核心上同时执行,彼此之间没有资源竞争,互不干扰。例如:8核CPU同时运行8个独立的计算任务。
- 并发:同一时间段内,多个线程/进程在单个或多个CPU核心上交替执行,本质是CPU通过时间片轮转实现"同时运行"的假象,必然存在资源竞争问题。例如:单核CPU同时处理浏览器、音乐播放器和编辑器三个程序。
核心区别:并行是"真同时",依赖多核硬件;并发是"假同时",依赖操作系统的调度算法。
二、计算机硬件底层原理
所有并发问题的根源,都来自于计算机硬件的物理限制:
- 电信号的串行本质:CPU内部的指令是高低电压信号(高=1,低=0),电压信号无法同时传输,必须排队执行。计算机所有硬件(CPU、内存、硬盘、网卡)之间的指令传输都遵循这一规则,任何一个硬件同一时间只能执行一个任务。
- 总线竞争:系统总线是连接各个硬件的"高速公路",同一时间只能有一个设备占用总线传输数据,这是硬件层面的资源竞争。
- 高速缓存的双刃剑:
- 作用:解决CPU运算速度与内存读写速度的巨大差距,将常用数据缓存到CPU高速缓存(L1/L2/L3),大幅提高CPU利用率。
- 问题:多个CPU核心的高速缓存之间会出现数据不一致(相互覆盖),这是可见性问题的硬件根源。
- 缓存行:高速缓存的最小存储单位是64字节,这意味着即使只修改1个字节,也会加载整个64字节的缓存行,可能导致"伪共享"问题。
三、线程状态与上下文切换
3.1 线程核心状态
所有编程语言的线程模型都基于操作系统的原生线程,核心状态统一:
- 新建态:线程对象已创建,但未调用
start()方法。 - 就绪态:调用
start()后,线程进入就绪队列,等待CPU调度。注意:进入就绪态的顺序不等于被CPU选中的顺序,操作系统会根据调度算法选择。 - 运行态:CPU分配时间片给该线程,开始执行代码。
- 阻塞态:线程因等待资源(锁、IO、sleep)而暂停执行,被移出就绪队列。
- 死亡态:线程执行完毕或抛出未捕获异常,生命周期结束。
关键:方法拷贝入栈是操作系统层面的行为,与编程语言无关。
3.2 上下文切换
这是多线程编程最主要的性能开销来源:
- 定义:CPU从一个线程切换到另一个线程时,需要保存当前线程的执行上下文(程序计数器、寄存器状态、栈信息),然后加载下一个线程的上下文。
- 时间损耗:单次上下文切换耗时约几毫秒到几十毫秒,操作系统给每个线程分配的时间片通常也是这个量级。
- 测量工具:
Lmbench3:精确测量上下文切换的时长vmstat:实时统计系统上下文切换的次数(输出中cs列)
- 减小上下文切换的方法:
- 无锁并发编程:通过数据分区避免资源竞争
- CAS算法:用乐观锁替代悲观锁,减少阻塞
- 使用最少线程:避免创建过多线程导致频繁切换
- 使用协程:用户态的轻量级线程,切换无需操作系统参与
四、多线程适用场景
多线程不是银弹,只有在特定场景下才能提升性能:
- IO密集型任务:非常适合多线程。例如:文件读写、网络请求、数据库操作。因为IO操作时CPU处于空闲状态,多线程可以让CPU在等待IO时处理其他任务,大幅提高CPU利用率。
- CPU密集型任务:不适合多线程。尤其是单核CPU,串行执行是最快的,多线程只会带来上下文切换的额外开销。即使是多核CPU,线程数也不应超过CPU核心数。
五、线程同步与锁机制
5.1 join()方法
作用:让调用线程等待目标线程执行完毕后再继续执行。
t1.start();
t2.start();
t1.join(); // 主线程阻塞,等待t1执行完
t2.join(); // 主线程继续阻塞,等待t2执行完
注意:上述代码中t1和t2是并行执行的,因为
t2.start()在t1.join()之前调用。join()只会阻塞调用它的主线程,不会影响已经启动的其他线程。
5.2 synchronized关键字
Java中最基础的悲观锁实现:
- 锁的对象:只能对引用类型加锁,基本类型无法加锁(因为基本类型存储在栈中,没有对象头)。
- 锁的范围:锁会保护整个同步代码块(大括号内的所有内容),只有当代码块执行完毕后才会释放锁。即使线程调用
sleep()进入睡眠状态,也不会释放锁。 - 特性:保证原子性、可见性和有序性,是"重量级"的同步机制。
5.3 sleep() vs wait()
这是面试高频考点,核心区别在于是否释放锁:
| 特性 | sleep() |
wait() |
|---|---|---|
| 所属类 | Thread类的静态方法 |
Object类的方法 |
| 锁行为 | 让出CPU,但不释放锁 | 让出CPU,并且释放锁 |
| 唤醒方式 | 睡眠时间结束自动唤醒 | 其他线程调用notify()/notifyAll() |
| 使用位置 | 任意位置 | 必须在同步代码块中 |
资源竞争失败的线程会进入阻塞队列,等待锁释放后被唤醒,重新进入就绪队列竞争CPU。
六、死锁与避免方法
死锁是指两个或多个线程互相等待对方释放锁,导致所有线程都无法继续执行的状态。
避免死锁的四个原则:
- 避免一个线程同时获取多个锁:尽量让每个线程只持有一个锁。
- 避免一个线程在锁内同时占用多个资源:保证每个锁只保护一个资源。
- 使用定时锁:用
Lock.tryLock(long timeout)替代synchronized,如果超时未获取到锁就放弃,避免无限等待。 - 数据库锁特殊注意:加锁和解锁必须在同一个数据库连接中进行,否则会出现解锁失败的情况。
七、内存模型与可见性
7.1 可见性问题
定义:当一个线程修改了共享变量的值,其他线程能够立即看到这个修改。
可见性问题的根源:
- 每个线程有自己的工作内存(对应CPU高速缓存)
- 线程修改共享变量时,先修改工作内存中的副本,再刷新到主内存
- 其他线程读取时,从主内存加载到自己的工作内存
如果没有同步机制,一个线程的修改可能永远不会被其他线程看到。
7.2 volatile关键字
轻量级的同步机制,被称为"轻量级的synchronized":
- 保证可见性:被
volatile修饰的变量,修改后会立即刷新到主内存,读取时直接从主内存读取。 - 禁止指令重排序:通过内存屏障实现,保证指令执行顺序与代码顺序一致。
- 不保证原子性:多个线程同时修改
volatile变量时,仍然会出现线程安全问题。
实现原理:基于总线嗅探机制,当一个线程修改了
volatile变量,其他线程会通过总线观察到该变量的内存地址被修改,从而使自己工作内存中的副本失效,下次读取时重新从主内存加载。
7.3 内存屏障
内存屏障是CPU层面的指令,作用是:
- 阻止屏障两边的指令重排序
- 强制将工作内存中的数据刷新到主内存
- 强制清空其他线程工作内存中的对应缓存行
八、原子操作与缓存机制
8.1 原子操作
定义:不可中断的一个或一系列操作,要么全部执行成功,要么全部执行失败,失败后会回滚到操作前的状态。
Java中的原子操作实现:
- 基本类型原子类:
AtomicInteger、AtomicLong - 引用类型原子类:
AtomicReference - 底层基于CAS(Compare-And-Swap)算法实现,是一种乐观锁机制。
8.2 缓存相关概念
- 缓存命中:处理器要处理的数据已经在高速缓存中,无需从内存读取,速度极快。
- 写命中:处理器要写回的数据在高速缓存中存在,直接修改缓存中的数据。
- 写缺失:处理器要写回的数据不在高速缓存中(可能被其他线程清理或覆盖),需要先从内存加载到缓存,再进行修改。
九、常用Linux命令
并发编程调试中常用的系统命令:
grep:文本筛选,用于过滤日志或命令输出awk:文本统计与处理,常用于分析性能数据sort:文本排序,配合其他命令使用jstack:生成Java线程快照,查看线程状态和死锁jmap:生成Java堆内存快照,分析内存泄漏
总结
并发编程的核心挑战是解决原子性、可见性和有序性问题。理解计算机硬件底层原理(CPU、缓存、总线)是掌握并发编程的关键。在实际开发中,不要盲目使用多线程,要根据任务类型(IO密集型/CPU密集型)选择合适的并发策略,同时注意避免死锁和减少上下文切换带来的性能损耗。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)