高并发编程底层原理详解(1)

CAS (compare and swap)比较并交换

在不加锁的情况,保持在多线程的一致性问题? => CAS
1.知识铺垫:i++在jvm 中是不安全的,不能保证原子性,在高并发的情况下,会存在问题。JMM(每个线程都会把主内存的数据copy的线程自己的空间中,执行完毕之后再写回主内存。)
2.解决方式 a.加锁 b.CAS
3.案列:AtomicInteger getAndAddIn()
4.流程解析:
i = 0
Thread A i++ ( 拿到i=0 ,并i = i+1 ,准备存入)还未存入
Thread B 在Thread A拿到I=0 之后,先存入到主内存中, 此时 主内存中的值 已经变为1
Thread A 此时发现主内存已经变为1了,于是,拿到i=1 ,重新执行 i = i+1 ,并写入到主内存中。(假设仅仅有2个线程。)
5.引发新问题 ABA 问题:
概念:线程a在最终写入主内存时,仅仅会比较当前值,而不关注他的变化过程(他可能由 0—>1>0)
解决方案:加一个版本号,乐观锁

   //如果不相等的话,则继续执行一遍,再拿出值,进行比较,直到相等 则写入。
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2); //native 实现。
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));//native 实现。

        return var5;
    }
//引出汇编指令(也就是说硬件直接支持)如果为多个cpu则加个lock
lock cmpxchg    => compare  exchange(lock保证原子性,其他cpu无法访问)
volatile 和syn  都是通过它实现
image.png

java在内存中的存储布局

1.Object o = new Object()在内存中占用多少个字节? => 16个字节(内容为空,比如说:int 4个字节,对象引用4个字节)
2.对象头 markword 8个字节(锁信息、分带年龄)(=>可以回顾一下垃圾回收过程:CMS 6 JVM 15 默认)
-- a.new -> 偏向锁-> 轻量级锁(无锁、自旋锁、自适应锁)->重量级锁 (syn的优化过程)
3.class 对象指针,指向class对象(默认开启压缩,被压缩 成4个字节,原8个)
4.实例数据:对象中存的数据、int 4个字节,string 4个字节(压缩后)
5.对齐(若不为8的倍数,则自动补齐,优化方式,8字节更快)

//依赖

            org.openjdk.jol
            jol-core
            0.10


//测试代码
  public static void main(String[] args) {
        Object obj = new Object();
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    }
image.png
image.png

Syn的优化过程

前置知识:由用户态 向 内核态申请重量锁 ,非常耗费资源。
1.无锁
2.偏向锁(存放线程指针、 偏向锁标记为1):线程执行任务是很快的,一般也不会冲突,因此,默认只需要加一个标记,而不需要上锁
3.轻量级锁(自旋锁,也就无锁):释放偏向锁,记录Lock Record (CAS )缺点:耗费CPU,适用于竞争不太激烈的情况
4.重量级锁,指向重量级锁(mutex 互斥量)的指针,锁标记为10,当竞争加剧、或者自旋超过 n次,则升级,每一个重量级锁都有一个队列,没被执行的任务、是不会占用cpu的
5.GC标记信息:CMS过程用到的标记信息 锁状态为11
6:上锁时,hashCode 会被 存放到LR中。

锁降级、锁消除和锁粗化

1.锁降级 -> GC的时候会发生
2.锁消除,如果JVM发现某个局部变量、并不会被其他线程调用,则会把syn取消
3.锁粗化,如果一直循环的在加锁、释放锁,JVM会优化成只在while中加一次锁。

Syn的最底层实现

linux +HSDIS 做测试
HSDIS 反汇编工具 HotSpot Di assembly
·JIT Just In Time 及时编译器 ,将热点代码直接编译成机器语言,而不在重新编译了。
最终会发现加了syn 或者使用了volatile变量 的方法 底层都是 lock cmpexh

Syn的实现过程

1.java代码:synchronized 关键字
2.字节码 monitorenter moniterexit
3.jvm 在执行过程中自动升级
4.硬件级 汇编语言 comxchg (compare and exchange)

Volatile

前置知识:
计算机组成原理: 核心就是CPU+内存+外设(硬盘、显示器、网卡等等。)
执行过程:内存中存放的就是指令,CPU获取、并执行指令。
PC寄存器 是存放下一条指令的位置

ALU:计算
cache:多级缓存( 金字塔结构 容量大、速度慢。 寄存器最快、硬盘最慢 (1:100w))
cache:就是缓存 用来提升访问速度的。
进程和线程的区别:线程是CPU执行的基本单位。进程是CPU分配资源的基本单位。
线程切换:Context Switch 当2线程进行切换时,需要将正在执行的线程的数据进行保存。再执行第二个线程。
超线程:一个ALU对应2组PC|Registers ,每个线程对应一组 PC|Registers ,因此 多线程进行切换时,并不需要保存当前线程的的数据。这就是所谓的四核八线程,因此线程切换的速度更快了。
缓存行 cache line:按行读取(获取变量时,会把相邻数据一同获取过来,最近优先原则)一行数据是64个字节。
缓存行越大,局部效率越高、但读取时间慢、
缓存行越小、局部性效率低、但读取时间快、(目前最优选择64字节)
MESI (E(exclusive)、M(modified)、S(shared)、I(invalid))缓存一致性协议、不同的CPU用的协议都是不一样的。(省略详细过程)

基础作用:
保证线程间的可见性。(每个线程都有自己的内存空间,获取变量时,会复制一份到内存中,当主内存中的值变化时,会同步到各个线程中。)

禁止指令重排:
概念:CPU的乱序执行,比如说 step1 需要耗费100ms step2 需要10ms。则step2可能与step1并行。则step2先执行完毕。(测试案列:略)
实现方式如下:
JVM:内存屏障
汇编:lock指令

源自马士兵2020年最新Java多线程高并发编程详细讲解公开课:
https://www.bilibili.com/video/BV1xK4y1C7aT?p=2

image.png

image.png

你可能感兴趣的:(高并发编程底层原理详解(1))