多线程与高并发学习一

参考链接-B站-马士兵老师

本内容是学习马士兵老师的B站公开课程的学习笔记,若有侵权,请联系删除

一 计算机基础

1 计算机结构

  1. 示意图
    在这里插入图片描述
  2. CPU组件
  • ALU 算术逻辑单元
  • Register
    寄存器,存储需要计算的数字
  • PC 计算器
    程序计算器,记录程序指令执行的当前位置
  • 其它:
    • CU MMU cache
  1. 程序的构成
    • 程序 = 指令 + 数据
  2. IO Bridge 数据总线
    • 从内存读入到CPU计算,要使用总线
    • 分类:
      • 控制线
        读指令
      • 地址线
        读取地址信息
      • 数据线
        读取数值

2 程序、进程与线程

  1. 程序
    • 一个程序可以在同一个机器跑两份,即两个进程;
  2. 进程
  • 需求:同一个进程内部,有多个任务并发执行的需求(一边计算、一边收集网络数据、一边刷新页面)
  • 计算机分时,实现多进程共同运行
  • 使用多进程实现功能并发处理,但是每个进程是有自己独立的内存地址空间的,多进程相互同步和共享数据的危险比较大,进程A 很轻易影响搞死进程B;
  1. 线程
  • 进程内部,有多个计算任务,共享进程的内存空间,但是不共享计算;方便了进程内部之间的数据同步和通信,并保持了进程与进程之前的独立和隔离。
  • 进程是静态的概念,程序进入内存,分配对应的资源:内存空间。进程进入内存会产生一个主线程
  • 线程是动态的概率,是可执行的计算单元。线程共享进程内部的内存资源。线程时指令,计算的数据在内存中放着。
  1. 线程切换(OS)
    在这里插入图片描述
    • 线程T1执行到哪条指令,数据状态,在切换到T2线程时,要保存T1线程的内容上下文、现场。下次切换回到T1后可以继续从原来的中止位置可以继续执行。 即线程切换时要保存上下文保存现场。
  2. 问题:
  • 是不是线程数量越多、执行效率越高?
    答:不是,比如一万个活的线程,要保存1万个上下文,切换更加频繁,会耗费过多的资源。
  • 对于一个程序,设置多少个线程合适?
    答:
  1. 计算设置合理线程数的理论计算公式
    在这里插入图片描述
    实际实践中,是通过压测来设置合理的值

3 (P7) CPU的并发控制:缓存一致性协议

3.1 CPU访问存储的速度

  1. ALU访问Register的速度是 ALU访问内存速度的100倍。即访问存储器的速度比是1:100;因为ALU距离Register很近。
  2. CPU缓存
  • LRU LFU内存淘汰策略
  • 为重复利用CPU的计算能能力,在CPU和内存之间添加缓存。缓存可以设置多层缓存
  • 示意图
    在这里插入图片描述
  1. 目前缓存设置有3级缓存;
    在这里插入图片描述
  • 目前,一般L1 L2缓存在CPU核内部。L3缓存是在一颗CPU的多个核之间共享。
  • 使用方式
    • ALU去Register读取数据,Register先去L1找,L1没有再去L2,L2没有再去L3,L3没有再去内存
    1. 超线程
      即一个ALU对应多组(Register + PC)5
    2. 缓存行
      即一次性读取的数据块,即1次缓存一行数据。依据:程序的局部性原理:空间局部性、时间局部性。
    • 缓存行的大小:过大过小都不好
      • 过大,读取速度慢,命中率高
      • 过小,读取效率高,但是命中率低
      • 工业界结论:一个缓存行 64Byte(2021),
        -
  1. 缓存一致性协议
  • 由于缓存行的存在,必须有一种机制来保证缓存数据的一致性,即缓存一致性协议。每个CPU厂商都有自己的协议实现,比如Intel 的 MESI
    在这里插入图片描述
  1. CPU级别的并发控制
    • 缓存一致性协议
    • 关中断
    • 系统屏障
    • 总线锁/缓存锁

3.2 马老师的课程体系

在这里插入图片描述

3.3 程序的执行顺序

  1. CPU的流水线设计
  2. CPU的乱序执行
  • 为了提高效率(在等待费时的指令的时候,可以去执行后面的指令)
  • 乱序是存在的证明示意图
    在这里插入图片描述
  1. 本地证明确实会出现乱序执行的场景。
  2. 理论: as-if-serial
    • 单个线程,两条语句,未必时按顺序执行;
    • 单个线程的重排序,必须保证最终一致性
    • as-if-serial 看上去像序列化(单线程)
    • 会产生的后果
      • 多线程会产生不希望看到的结果
  3. 哪些指令可以互换顺序
  • happens-before原则,
  1. 双检锁 单例模式-Double Chekc Lock 存在的问题
  • 线程1创建懒汉模式创建对象;对象只创建了一半,还没有完成初始化完成,线程2判断实例非空,就会直接拿着该实例了一半的对象去使用,出现BUG。
  • synchronized可以保证代码块内部的原子性和可见性,但是无法保证代码的有序性;
  1. 放置乱序执行的方式
  • 禁止编译器乱序

  • 使用内存屏障阻止指令乱序
    • 内存屏障是特殊指令,看到这种指令,前面的必须执行完,后面的才能执行。intel:ifence sfence mfence(CPU 特有指令)

    • JVM中的内存屏障

      • 所有实现JVM规范的虚拟机,必须实现四个屏障
      • LoadLoadBarrier LoadStore SL SS
  • volatile的底层实现
    • volatile修饰的内存,不可重排序,对volatile修饰的变量读写访问,都不可以换顺序;
    • hostspot实现源码:C++代码中
  1. JVM 的内存屏障
  • Load Load
  • Store Store
  • Load Store
  • Store Load
    总结,这些只是进程中的操作,关键还是需要底层的OS和CPU的支持
  1. Volatile 可见性
    • 保证可见性
    • 禁止重排序
      Volatile实现原理
  2. volatile的实现细节
    在这里插入图片描述
    • 上述只是JVM层面的实现,不同的JVM有不同的实现,hotspot的实现原理为:
  3. CPU级别的并发控制方法
    • 关中断
    • 缓存一致性协议
    • 系统屏障
      包括编译级别、指令级别
    • 总线/缓存锁
      lock 是总线或缓存锁
  4. 参考书
    编码,隐匿在计算机背后的语言

4 (P14) synchronized&AQS

4.1 CAS

  1. CAS 基本示意图
    compare and swap
    compare and set
    compare and exchange
    在这里插入图片描述
  2. 作用
    • 通过使用CAS来实现了多线程情况下不阻塞每个线程并
    • 且实现了对数据的并发访问设置值;
    • CAS:
  3. ABA 现象
    • 线程1 将变量num 设置为A,又设值为B 有改成A,对于线程2,如何识别线程1 是否真正变化了? 添加版本号,即线程2不仅仅看值还看版本号; 或添加标志位flag;
  4. CAS的底层实现
  • CAS 的底层实现和synchronized,volatile的底层实现都是一致的。
  1. 汇编源码实现
    • lock cmpxchg 指令 (汇编指令)
    • lock从硬件级别上保证了一个线程在修改值的时候,其他线程不能去修改;cmpxchg 自己独立不能保证原子性
    • 硬件层面:lock指令在执行后面的指令的时候锁定一个北桥信号(不采用锁总线的方式)
      在这里插入图片描述
  2. 对象在内存中的布局
    在这里插入图片描述
    • 起始的8Byte是 MarkWord
      在这里插入图片描述
  • 问题
Object o = new Object();
问题一:O 在内存中占用多少个字节?
   答:对象头Markword 占用8个字节,对象的指针一般占用8或者4个字节,如果JVM开启了指针压缩,则指针渣勇4个字节,由于一个对象占用的字节数必须要为8Byte的整数倍,所以会再补充4个字节,Padding。 所以一共占用16Byte。如果没有开启指针压缩,则指针也占用8字节,再加上Markword8个字节,一共也是16个字节。 如果一个对象还有成员变量,则会在上述基础上再添加成员属性锁占用的字节数,然后再视情况是否要padding。 同时注意,一个对象添加方法是不占用字节的,只有对象的成员变量会占用字节。

4.2 synchronized锁升级的过程

  1. synchronized锁信息存放在markword中
  2. 有一个锁升级的过程:
    • 无锁(刚创建对象时)、偏向锁(无锁、自旋锁、自适应锁)、轻量级锁、重量级锁
    • 升级过程与markword关系密切(markword一共8byte, 64bit)
      在这里插入图片描述
  3. 锁状态
当前状态1bit 偏向锁位2bit 锁标志位
无状态 (new)001
偏向锁101
轻量级锁-00
重量级锁10
GC标记信息11
  1. 升级明细过程
a. 默认synchronized(0)
00-> 轻量级锁
默认情况下,偏向锁有个时延,默认是4S。 因为JVM自己有一些默认的启动线程,里面有很多sync代码,这些ysnc代码启动时就知道肯定会有竞争,如果使用偏向锁,就会造成偏向锁不断的进行锁撤销和锁升级的操作,效率较低
b. 如果设置 -XX:BiasedLockingStartDealy=0,newObject() ->101偏向锁 -> 线程ID0->Anonymous BiasedLock 打开偏向锁,new处理的对象,默认就是一个可偏向锁你们对象101
c.如果有线程上锁
  上偏向锁,指的就是把markword的线程ID改成自己的线程ID的过程。 偏向锁不可重偏向、批量偏向、批量撤销
d.如果有线程竞争
   撤销偏向锁,升级到轻量级锁
   线程再自己的线程栈生成LockRecord,用CAS操作将markword设置为指向自己的这个线程的LR的指针,设置成功者得到锁。
e.如果竞争加剧
  竞争加剧:有线程超过10次自旋,-XX PreBlockSpin,或者自旋线程数超过CPU核数的一半,1.6之后加入自适应自旋 。如果太多线程自旋会导致CPU消耗过大,不如升级为重量级锁进入等待队列(不在消耗CPU)。Adapative Self Spinning, JVM自己控制
  升级到重量级锁->OS申请资源,linux mutex, CPU3-0级系统调用,线程挂起,进入等待队列,等到OS的调度,然后再映射回用户空间。涉及到用户态切换到内核态,再切换回到用户态的过程。
  1. 锁降级
    在GC发生时会存在,除了GC线程访问,其他线程不会访问,所以降级没有意义了。可以认为没有降级也可以。
  2. 锁消除 lock eliminate
  • 示例
    private String add(String s1,String s2){
        StringBuffer sf = new StringBuffer();
        sf.append(s1).append(s2);
        return sf.toString();
    }
  • StringBuffer的 append方法是被synchronized修改过的,但在上面的代码中,sf只会在add方法内部引用,不可能被其他线程引用(因为是局部变量,线程栈私有),因此此处的sf是不可能共享的资源,JVM会自动消除StringBuffer对象内部的锁。
  1. 锁粗化 lock coarsening
  • 示例
  private String test(String str) {
        int i = 0;
        StringBuffer sf = new StringBuffer();
        while (i < 1000) {
            sf.append(str);
            i++;
        }
        return sf.toString();
    }
  • 解释:JVM会检测到这样一辆车的操作都是对同一个对象加锁(while 循环内100次执行append,)此时JVM就会将加锁的范围粗化到这一连串的操作的外部(比如while虚幻体外),使得这一连串的操作只需要加一次锁即可。
  1. synchronized vs Lock(CAS)
    • 在高竞争 高耗时的环境下 synchronized效率更高
    • 在低竞争 低耗时的环境下CAS效率更高
    • synchronized 到重量级之后是 等待队列(不消耗CPU),CAS(等待时间消耗CPU)
  2. synchronized实现过程
  • java代码: synchronized关键字
  • class字节码层面:monitorenter,monitorexit
    即只添加了这两个指令
  • 执行过程中自动升级:
  • 在CPU汇编层级,使用lock comxchg 指令来实现保证原子操作,锁实现。

4.2 计算机基础

  1. 超线程
    • 一个ALU对应多个PC核Register组,即所谓的四核八线程
    • 进程:资源分配的基本单位
    • 线程:CPU进行执行调度的基本单位
  2. cache line
    在这里插入图片描述
  • 按照块来读,目前是64Byte,即一行数据64字节;
  • 比如MESI协议:cpu的每个cache line标记四种状态(额外的2个bit);
  • 其他缓存一致性协议:MSI MESI MOSI Synapse Firefly Dragon等。

4.3 volatile关键字

  1. 作用
  • 保证一个变量的CPU可见性,保证线程可见性。
  1. 超线程
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值