并发系列 0:某些你需要先了解的概念
并发和并行的区别
并行(parallel):在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是宏观角度来看,二者都是一起执行的。
并发(concurrency):在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果,但微观上并不是同时执行的,只是把时间分为若干段,使得多个线程快速交替的执行。
但是二者的目的都是为了最快化CUP的使用效率
为什么需要多线程
喜欢睡觉的朋友都知道,睡觉容易醒来难。这是由于我们身体的延迟习惯。
而 CPU、内存、I/O 设备之间的速度也有很大的差异。所以为了合理利用 CPU 的高性能,尽可能的减少三者之间的速度,计算机体系结构、编译系统、操作系统都做出了相应的改善。譬如:
- CPU 增加了三级缓存,用于均衡与内存的速度差异
- 编译程序新增优化执行次序(如编译器优化、指令并行重排、内存系统重排),使得缓存能够得到更加合理得使用
- 操作系统增加了进程、线程,以时分复用 CPU、进而均衡 CPU 与 I/O 设备之间的速度差异。
并发三大特性
以上做出的三大改善,便会引出并发的三大特性。
-
可见性:当一个线程修改了共享变量的值,其他线程也能看到修改的值
此时主存中 i= 0; CPU1 执行 i = i + 3; CPU2 执行 i = i + 5;
cpu1 将结果写入高速缓存中,但还没写入主内存中时。 cpu2 此时从主内存中获取了 i = 0 加载进自己的高速缓存中,这样就会导致返回的结果并不符合预期的 i = 8 的结果集。
从而引发了可见性问题。
那么如何保证有序性呢(从 Java 角度出发)?
- volatile 关键字
- 内存屏障
- synchronized 关键字
- Lock 关键字
- final 关键字
-
有序性:即程序执行的顺序按照代码的先后顺序执行
int i = 0; boolean flag = false; i = i + 3;
以上代码,初看没有什么问题,实际上也没什么问题。 但在 jvm 执行后可能会发生顺序重拍,因为 cpu 在执行第三步的时候,高速缓存失效,要重新从主存的值,这样就会导致慢了一丢丢。 所以可能会将第一步和第二步调换(有意思的是,笔主在查看字节码的时候并没有发现变化,因此推断可能发生在后两阶段的重拍)。
因此,为了提高执行的性能,处理器和编译器通常会对指令进行重新排序。重排又分为一下三种:
- 编译器优化重排:编译器在不改变单线程语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行重排:现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统重排:由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
一条 Java 命令执行到最终执行,便是顺序经过以上三种指令排序。
上诉的后两种重排方法属于处理器重排。
对于编译器重排序,JMM 的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。
而对于处理器重排序,JMM 的处理器重排序规则会要求 java 编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers,intel 称之为 memory fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。
那么如何保证有序性呢?
- volatile 关键字
- 通过内存屏障
- synchronized关键字
- Lock
-
原子性:一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行。在 Java 中,对基本数据类型的变量的读取和赋值操作是原子性操作(64位处理器)。不采取任何的原子性保障措施的自增操作并不是原子性的。
int i = 0; // 线程1.2 执行 i ++;
通过以上代码, i++ 虽然只有一行代码,但是却需要三行 CPU 代码:
- 读取变量 i
- 在 CPU 寄存器中执行 i + 1 操作
- 将结果写入内存/缓存中
由于线程的上下文贴换,在线程1执行了第一条指令后,便切换到了线程2执行。 这样便会导致原子性问题。
那么如何保证原子性呢?
- synchronized 关键字。
- Lock。
- CAS。
注意:为什么会特别标注64位处理器呢?
主要在32位处理器在计算64位数据时,会将其分为高低位两次单独的写入。这可能导致线程看到前32个来自与上一个64位的值,后32位来自另一个。
但是,在Java5之后,开发人员 JMM 进行了优化,确保了单个读写(笔者认为就是确保在同一时间段,只有一个64位分成的高低位在进行运算)的原子性,也就是以上错误并不会出现,但他对于其他复杂操作并不包含原子性。
好啦,概念先介绍到这里,更详细的说明就等笔者的后续文章吧。
其实最主要的是笔者还没形成总结,也还在思考一些概念。
参考资料
《Java并发编程的艺术(方腾飞)》 Java 并发和多线程教程 (jenkov.com)
转载自:https://juejin.cn/post/7384636994981609510