1. 并发理论基础:并发问题产生的三大根源
1. 并发问题的起因:最大化利用CPU
CPU运算速度和IO速度不平衡,导致一个任务执行时大部
分时间都在等待IO工作完成,CPU资源无法合理运用起来。
2. 进程+线程,CPU时间片:线程切换
进程是操作系统中资源分配的最小单元,线程是进程中的执行单元,一个进程包含多个线程;
线程共享进程的资源,如内存、文件句柄等。线程之间切换开销较小,因为共享进程的资源。
CPU时间片:为了更合理公平的把CPU分配到各个线程,CPU把时间分为若干单位的片段,提高利用率。
3. 并发问题根源之一:CPU切换线程导致的原子性问题
Int number=0;
number=number+1; // 编译器将代码拆成多个指令交给CPU执行
4. 高速缓存的产生:减少CPU等待IO的时间
为了减少CPU等待IO时间,充分利用CPU资源,在内存的基础上增加CPU级别的缓存(L1,L2,L3缓存);
CPU高速缓存用于减少处理器访问内存所需时间,容量远小于内存,但访问速度确实内存IO的上百倍。
5. 并发问题根源之二:缓存导致的可见性问题
多核CPU中,每个核心处理器都有自己的CPU缓存,互不可见;
多线程情况下线程并不一定在同一个CPU上执行,当同时操作一个共享变量时,就会导致缓存不可见问题。
6. 指令优化(重排序):调整指令顺序来提升指令执行效率
大体逻辑:先执行比较耗时的指令,在这些指令执行的空余时间来执行其它指令;
就像做菜时先把熟的最慢的菜最先开始煮,然后在这个菜熟的时间段去做其它的菜;
通过这种方式减少CPU的等待,更好的利用CPU的资源。
7. 并发问题根源之三:指令优化导致的重排序问题
value=8;
flag=true; //这两个语句直接没有依赖关系,指令重排后可能先执行flag=true
2. 并发基础理论:缓存可见性、MESI协议、内存屏障、JMM
1. 缓存可见性的问题
解决缓存可见性问题,本质上是要解决一个CPU修改了数据如何让其他CPU知道,
然后多个CPU同时修改缓存数据如何保证他们操作的有序性。
2. 通过总线保证一致性
CPU要和存储设备交互,必须通过总线设备,在获得总线控制权后才能启动数据信息传输;
两种方案:a. 总线嗅探:当一个CPU修改主存数据时,总线会广播通知其它CPU失效缓存数据;
b. 总线仲裁:多个CPU申请总线使用权,来保证多个操作的互斥。
3. 总线性能问题优化方案
总线性能瓶颈在于总线与主存打交道会造成阻塞,优化的核心在于减少通过总线与主存交互的操作;
两种思路:a. 减少从主存读取数据频率(优先从其它CPU缓存读)
b. 减少修改数据同步主存频率(合并写)
4. MESI协议(缓存一致性协议)
缓存一致性协议机制:通过自己的数据状态就能知道其它CPU的缓存情况,从而做出对应策略。
MESI协议通过对共享数据进行不同状态的标识,来决定CPU何时把缓存的数据同步到主存,何时可以从缓存
读取数据,何时又必须从主存读取数据;MESI每个字母代表着一种数据状态,分别是:
a. Modified(独占状态) :只有自已缓存了数据,放心读,也不着急同步主存;
b. Share(共享状态):其它CPU也缓存了该数据,数据还未被修改,还是最新的;
c. Modified(修改状态):中间态,数据是最新的,其它CPU缓存都是invalid态;
d. Invalid(无效状态):当前缓存行被其它CPU修改了,当前缓存数据已经失效。
5. MESI协议的优化:Store Buffere + 失信队列
MESI在修改数据的时候必须先广播,然后等待其他所有CPU都把数据标记失效后,才能进行数据的修改操作,
这个过程比较耗费时,所以为了提升CPU的利用率,同时减少广播等待的时间,就增加了store buffer(广
播后不等其它CPU回复就干别的)和失效队列(invalid广播消息来了先放队列再慢慢处理)来进行优化。
6. 内存屏障:对少数场景下禁用CPU缓存优化
MESI优化后提升了整体性能,但原来的数据强一致性变成了弱一致性,少数情况下CPU缓存仍然存在不一致;
Store Barrier(写屏障指令):把store buffer的数据都同步到内存中取;
Load Barrier(读屏障指令):读取共享变量前,先处理完失信队列,保证读的都是最新数据;
Full Barrier(全能指令):包含读写屏障指令。
7. JMM(Java内存模型):Java对内存屏障指令的封装
因为内存屏障是操作系统级别的指令,而不同的操作系统,内存屏障的指令又不一样,为了避免程序员花费太
多的精力在这些内存屏障指令上,所以Java就封装了一套java的内存屏障,把不同操作系统的指令都封装在
内,对程序员暴露的是一套统一的指令规范。
Java中有以下内存屏障指令的关键字:
a. volatile:保证变量的可见性,禁止指令重排序;
b. synchronized: 保证代码块的原子性和可见性,禁止指令重排序;
c. final: 保证变量的不可变性,禁止指令重排序;
d. Unsafe: 提供了一系列底层操作,如CAS(比较并交换)、内存屏障等。
3. 并发基础理论:Java内存模型JMM
4. 深入理解volatile
从全局上来说,并发层面的问题主要包含三个:
一是CPU切换指令执行导致的原子性问题;
二是CPU缓存导致的可见性问题;
三是编译器和操作系统进行指令优化导致的指令重排序问题。
1. 解决指令重排问题
volatile遵循了JMM规范,修饰的变量禁止指令重排,底层原理是通过操作系统级别的内存屏障指令实现。
2. 解决缓存可见性问题
缓存锁实现:MESI协议->MESI协议优化:Store Buffere+失信队列->内存屏障(解决MESI优化带来的问题)
3. volatile总结:
a. 用volatile定义的共享变量生成的指令不允许进行重排序,从而保证指令的顺序性;
b. 在对volatile定义的变量进行修改时,会加上写屏障(或全能屏障),保证修改的共享变量会马上对其他CPU暴露;
c. 在对volatile定义变量进行读取的时候,会加上读屏障,从而保证读取的共享变量值是最新的。
5. 并发工具(锁):深入Synchronized
synchronized锁的升级过程:
synchronized 的执行原理示意图:
6. 并发工具(锁):深入Lock+Condition
1. lock与synchronized对比
相同的设计模型:都是基于管程模型设计,实现同步、互斥的思路是一样的;
等待队列数量不同:sync只有一个队列,lock与Condition结合,可以创建多个条件队列;
公平非公平差异:sync是一种非公平锁,lock对公平和非公平进行了实现;
性能差异:sync进行锁升级优化后,性能差异不大;
死锁问题:lock支持尝试获取锁,更灵活,tryLock()、tryLock(time)。
2. lock原理分析:锁状态(state)+ 等待队列(AQS)