高并发学习

高并发学习笔记

出自b站马士兵的相关教程

1.程序运行的底层原理

计算机的组成

程序是什么? –> XX.exe (躺在磁盘里)

进程是什么? –> “程序启动,进入内存” 资源分配的基本单位(似乎为数据+指令)

线程是什么? –> 程序执行的基本单位

程序如何开始运行? –> CPU读指令:PC存储指令地址、Register读数据、ALU进行数学计算,回写,下一条

程序如何调度?

–> 原始时所有app可以直接操作硬件,为了管控这些app,出现了操作系统。没当某个app产生某个线程,操作系统就会进行线程调度,决定哪个线程放入哪个cpu执行。

但是上方这个调动不是a做完再换b,而是a做一会儿再换b再换回a这样。为了保存上一次操作某个线程的现场,就需要把上次操作的情况存入cache(缓存)。这些叫做线程的切换

2.CAS(compare and swap)的实现

image-20220825213702227

表层逻辑如上图

和sympathizer和volatile一样,底层实现逻辑都是靠lock指令在硬件级别锁定一个北桥芯片。

内存布局:

可以使用openJDK里的工具JOL来直接输出某个对象的内存布局。其maven调用如下:

JOL

比如要查询一个新创建的Object的布局,查看代码如下:

image-20220825150451786

(下面那一行被注释的代码可以输出后发现markword改变,即得锁的信息储存在对象头里,如下图)

image-20220825192144597

此处可以先了解对象在内存中是这样存储的,如图:

markword大小为8个字节64位,

类型指针本来是8字节,但由于默认的压缩机制,存储时为4字节,

最后会通过加空白让对象的大小为8字节的整数倍。

image-20220825151442362

3.锁升级的过程

锁和GC标记信息都会被存储在对象头markword里,其中锁的种类有下图几种

image-20220825202526014

【ps:上图中有一个hashcode,这东西是用来给对象分类的,方便在使用的过程中查找。同类的对象会有相同的hashcode,在再某堆hashcode相同的对象里通过equals()找到指定的对象。从而提高查找效率】

一个对象刚刚new出来的时候为无锁态(无锁态和下面的无锁不是一种东西)

当第一个线程使用某个对象时,使用的锁为偏向锁,即标记上该线程自己的标志(指向当前线程的指。针),其他线程发现这个对象时就不会使用。这样就不需要去向操作系统申请重量级锁,从而提升了效率。(这种锁适用于没有发生竞争的时候,即实质上从头到尾只有第一个线程使用了该对象)

当发生了其他线程与第一个线程的竞争时,偏向锁会升级为轻量级锁(又叫自旋锁无锁)其过程为:撤销之前的偏向锁,原线程和新线程都在自己的线程栈里生成自己的一个对象lock record(LR),两个LR发生争抢,这个争抢是自旋过程:某个线程先读出对象的锁的位置里原来的值,改成一个指向自己LR的指针,再尝试往回写。在回写之前检查那个位置的值是否和之前读的一样,如一样则争抢成功。失败方会反复重复自旋读写的过程,当然在成功者结束前会它还会一直失败。

当某个自旋的锁自旋超过10次或者在自旋等待的锁超过了cpu核数的1/2,则轻量级锁会被升级为重量级锁(这个规则在jdk1.6之后加入了自适应自旋,由jvm自己判断)。

操作系统一般分为用户态和内核态(两者之间互相切换),内核态用来执行大部分那些直接操作硬件的任务,用户态来运行应用程序。当要操作硬件时,由内核代为执行。而锁在内核中是一群有数量限制的互斥的数据结构。只有应用程序等想内核申请使用重量级锁,内核才能给予它们锁,即一个指向重量级锁的指针。

重量级锁内含队列,它可以让之前在自旋的线程们在队列中停止等待(wait或阻塞),从而不再持续消耗cpu。当轮到某个线程时由操作系统指定打开。

锁降级会在一些特定的情况下发生(GC的时候)(Garbage Collection),没什么意义。

关于锁还有两个概念分别叫做逃逸分析同步省略,同步省略又被叫做锁消除,相关的具体解释可以查看此处:深入理解Java中的逃逸分析锁粗化的原理作用也差不多,此处不多赘述。

(上面这个链接里还顺便解释了JIT即时编译技术,也就是把热点代码直接翻译成机器语言缓存起来)

2022/8/25 21:49


4.缓存行(cache line)的概念

计算机在读取数据的时候从main mamory -> L3 Cache -> L2 Cache -> L1 Cache 的过程是按块读的,即除了当前需要的数据本身还有其后面的一片,这样就可以避免之后要用到后面数据时反复上方的过程。而这一个块就被成为一个cache line,大小为64字节。

在cpu层级中,为了维持数据的一致性,如果有两个线程同时对同一个cache line里的数据进行了修改,当一个线程发生了修改后,它要通知其他线程这个修改。因此,如果我们使用继承等手段把一个对象的大小刚好凑在(放大到)64字节,使之不可能与其他对象在同一缓存行,修改之时不会互相影响,运行速度反而会变快。这种写法叫做缓存行对齐。有一个框架叫做disruptor(闪电)底层用了这个原理。


   转载规则


《高并发学习》 狐狸狐涂 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
下一篇 
LeetCodeEveryDay LeetCodeEveryDay
LeetCode每日任务(持续更新) 这是一个用来完成leetcode每日打卡的打卡点,用来记录难度不高的题目。如果有难度价值比较高的题目再单独开新贴整理。 1408. 数组中的字符串匹配 看到这题时感觉和之前的赎金信有异曲同工之妙,自
  目录