如何学习并发编程
很多框架或者自研组件的底层,都或多或少涉及到并发编程方面的技术点。
并发编程掌握不好,工作中难免会遇到一些问题,比如:
- 程序本地跑起来没有 Bug,但是一到多线程环境下就乱了;
- 老大让你去优化接口,每次都没有头绪;
- JUC 的各个 API 都会用,但是不了解内部原理机制;
- 面试问到并发编程就得跪;
- ……
相信很多朋友学习并发编程都背过八股文,但这些都只是停留在技术浅层,并发编程的学习,核心在于应用,并且构建起完善的、可以应付各种问题的知识体系。
梳理并发编程的核心知识体系,并且在工作中,尝试将这些学习到的技术点落地到实际项目中,例如:
- 从 0 到 1 搭建了上百容器节点的线程池监控体系;
- 去落地实现了一套分布式id生成器;
- 遇到一些比较耗时的计算任务时,通过合理使用异步编程的某些技术点进行性能优化;
- 理解了线程上下文的原理后,在分布式环境下定制 traceId 对请求链路进行监控;
- 采用多线程+消息队列技术实现分库分表技术;
- 利用 fork/join 技术去计算一些大量级数据任务;
- ……
Java语言体系内的各种并发编程技术,其实都是在对底层技术原理的封装,虽然API名称会有所出入,但是如果你对底层足够了解的话,会发现其实它们大多数都是相通的。而且,并发编程的很多设计与实现,都是源自于操作系统内部的需求所发明的,例如管程、数据一致性、产生并发的原因等。
程序和CPU之间的协作关系
计算机的运作通常都离不开CPU的扶持,一旦离开了CPU,程序就无法运行
这里有一段简单的Java程序:
public class CountDemo {
public static void compareTest(int a, int b, int[] arr) {
int t1 = countSum(a);
int t2 = countSum(b);
if (t1 > t2) {
arr[0] = 1;
} else {
arr[1] = 1;
}
}
//1~a的求和计算
public static int countSum(int a) {
int sum = 0;
for (int i = 1; i <= a; i++) {
sum += i;
}
return a;
}
public static void main(String[] args) {
int a[] = {-1,-1};
compareTest(1, 2, a); // ----- code_1
}
}
这份程序在IDE工具开发完毕后,实际上会被保存在磁盘当中。接着如果要运行程序的话,可以输入一段 Java 指令去运行它,如:javac 和 java 指令。接下来磁盘中的程序会被读取到内存当中,并且进行相关的编译。期间会涉及到多次编译,会先在 jvm 层变成 class 字节码,然后再转换为汇编指令,最后才是到机器码(这也正是将Java代码转换为汇编指令才能认清它背后原理的原因了)。
这些机器码存放的地址会被放到一种叫做程序计数器的寄存器中,之后控制器会到根据程序计数器的地址去读取相关的机器指令,并且将指令读取给运算器进行计算。当运行结束之后,程序计数器的地址就会刷新,让控制器去加载新内存地址的指令给到运算器。
首先 main 函数执行,程序计数器的地址会更新为 main 函数的入口位置,让控制器去加载其指令地址开始执行。接着在准备调用 compareTest 函数的时候,会有一条 call指令,将当前的程序计数器地址变更为子函数的入口地址,同理,在 comparetTest 函数内部调用 countSum 函数也是会发送 call指令。当子函数执行结束后,便会执行一条 return指令,返回到原先执行代码位置的下一条指令位置(call指令和return指令在函数调用的过程中是经常会使用到的)。
我们深入分析 countSum 内部,会发现它包含了求和累积的计算,这里边涉及到了累加寄存器和标志寄存器的使用。累加寄存器这个很好理解,就是将sum的数值在累加寄存器中不断更新。而标志寄存器其实是用于了判断是否满足跳出循环的逻辑。
我们在 for 循环代码中所编写的 i<=a; 这个逻辑,而计算机底层会通过做差的方式来判断是否满足该条件,也就是变成了:a - i >= 0; 的判断(这里有些数学中的不等式基础运算的味道~)。而通过 a-i 计算出来的结果会被记录在标志寄存器中的某些个位上
我们可以将标志寄存器理解为是一个巨大的 bit 数组,不同位置上的 bit 值表示不同的含义,当需要将计算结果记录为负数的话,只需要将 bit[0] 更新为 1 即可。
接下来我们来看看变址寄存器和基址寄存器,这两个寄存器主要是在数组进行定位元素的过程中会有所使用。
CPU 在对数组这类数据结构的内部元素进行定位的时候会通过基址寄存器的位置 + 变址寄存器的数值进行查询,变址寄存器就有点类似于是数组的索引下标,通过一个相对偏差的数值去对具体位置的定位。
通用寄存器这块其实比较好理解 , 大家可以将它理解为用于专门存储一些临时变量的公共部分,例如一些临时定义的数字值,对它进行深入了解的意义并不是很大,大家知道有这么一个东西就可以了。