并发学习笔记 -- 老王

212 阅读7分钟

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

image.png


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

image.png

4. 深入理解volatile

从全局上来说,并发层面的问题主要包含三个:
      一是CPU切换指令执行导致的原子性问题;
      二是CPU缓存导致的可见性问题;
      三是编译器和操作系统进行指令优化导致的指令重排序问题。
      
1. 解决指令重排问题
      volatile遵循了JMM规范,修饰的变量禁止指令重排,底层原理是通过操作系统级别的内存屏障指令实现。
      
2. 解决缓存可见性问题
      缓存锁实现:MESI协议->MESI协议优化:Store Buffere+失信队列->内存屏障(解决MESI优化带来的问题)
      
3. volatile总结:
      a. 用volatile定义的共享变量生成的指令不允许进行重排序,从而保证指令的顺序性;
      b. 在对volatile定义的变量进行修改时,会加上写屏障(或全能屏障),保证修改的共享变量会马上对其他CPU暴露;
      c. 在对volatile定义变量进行读取的时候,会加上读屏障,从而保证读取的共享变量值是最新的。

image.png image.png

5. 并发工具(锁):深入Synchronized

image.png image.png image.png image.png synchronized锁的升级过程: image.png synchronized 的执行原理示意图: image.png

6. 并发工具(锁):深入Lock+Condition

1. lock与synchronized对比
      相同的设计模型:都是基于管程模型设计,实现同步、互斥的思路是一样的;
      等待队列数量不同:sync只有一个队列,lock与Condition结合,可以创建多个条件队列;
      公平非公平差异:sync是一种非公平锁,lock对公平和非公平进行了实现;
      性能差异:sync进行锁升级优化后,性能差异不大;
      死锁问题:lock支持尝试获取锁,更灵活,tryLock()、tryLock(time)。
   
2. lock原理分析:锁状态(state)+ 等待队列(AQS)

image.png

7. Java自我修养-线程池