并发编程一
并发编程核心知识点笔记一、基础概念并发 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.1join()方法作用让调用线程等待目标线程执行完毕后再继续执行。t1.start();t2.start();t1.join();// 主线程阻塞等待t1执行完t2.join();// 主线程继续阻塞等待t2执行完注意上述代码中t1和t2是并行执行的因为t2.start()在t1.join()之前调用。join()只会阻塞调用它的主线程不会影响已经启动的其他线程。5.2synchronized关键字Java中最基础的悲观锁实现锁的对象只能对引用类型加锁基本类型无法加锁因为基本类型存储在栈中没有对象头。锁的范围锁会保护整个同步代码块大括号内的所有内容只有当代码块执行完毕后才会释放锁。即使线程调用sleep()进入睡眠状态也不会释放锁。特性保证原子性、可见性和有序性是重量级的同步机制。5.3sleep()vswait()这是面试高频考点核心区别在于是否释放锁特性sleep()wait()所属类Thread类的静态方法Object类的方法锁行为让出CPU但不释放锁让出CPU并且释放锁唤醒方式睡眠时间结束自动唤醒其他线程调用notify()/notifyAll()使用位置任意位置必须在同步代码块中资源竞争失败的线程会进入阻塞队列等待锁释放后被唤醒重新进入就绪队列竞争CPU。六、死锁与避免方法死锁是指两个或多个线程互相等待对方释放锁导致所有线程都无法继续执行的状态。避免死锁的四个原则避免一个线程同时获取多个锁尽量让每个线程只持有一个锁。避免一个线程在锁内同时占用多个资源保证每个锁只保护一个资源。使用定时锁用Lock.tryLock(long timeout)替代synchronized如果超时未获取到锁就放弃避免无限等待。数据库锁特殊注意加锁和解锁必须在同一个数据库连接中进行否则会出现解锁失败的情况。七、内存模型与可见性7.1 可见性问题定义当一个线程修改了共享变量的值其他线程能够立即看到这个修改。可见性问题的根源每个线程有自己的工作内存对应CPU高速缓存线程修改共享变量时先修改工作内存中的副本再刷新到主内存其他线程读取时从主内存加载到自己的工作内存如果没有同步机制一个线程的修改可能永远不会被其他线程看到。7.2volatile关键字轻量级的同步机制被称为轻量级的synchronized保证可见性被volatile修饰的变量修改后会立即刷新到主内存读取时直接从主内存读取。禁止指令重排序通过内存屏障实现保证指令执行顺序与代码顺序一致。不保证原子性多个线程同时修改volatile变量时仍然会出现线程安全问题。实现原理基于总线嗅探机制当一个线程修改了volatile变量其他线程会通过总线观察到该变量的内存地址被修改从而使自己工作内存中的副本失效下次读取时重新从主内存加载。7.3 内存屏障内存屏障是CPU层面的指令作用是阻止屏障两边的指令重排序强制将工作内存中的数据刷新到主内存强制清空其他线程工作内存中的对应缓存行八、原子操作与缓存机制8.1 原子操作定义不可中断的一个或一系列操作要么全部执行成功要么全部执行失败失败后会回滚到操作前的状态。Java中的原子操作实现基本类型原子类AtomicInteger、AtomicLong引用类型原子类AtomicReference底层基于CASCompare-And-Swap算法实现是一种乐观锁机制。8.2 缓存相关概念缓存命中处理器要处理的数据已经在高速缓存中无需从内存读取速度极快。写命中处理器要写回的数据在高速缓存中存在直接修改缓存中的数据。写缺失处理器要写回的数据不在高速缓存中可能被其他线程清理或覆盖需要先从内存加载到缓存再进行修改。九、常用Linux命令并发编程调试中常用的系统命令grep文本筛选用于过滤日志或命令输出awk文本统计与处理常用于分析性能数据sort文本排序配合其他命令使用jstack生成Java线程快照查看线程状态和死锁jmap生成Java堆内存快照分析内存泄漏总结并发编程的核心挑战是解决原子性、可见性和有序性问题。理解计算机硬件底层原理CPU、缓存、总线是掌握并发编程的关键。在实际开发中不要盲目使用多线程要根据任务类型IO密集型/CPU密集型选择合适的并发策略同时注意避免死锁和减少上下文切换带来的性能损耗。