并发编程理论基础
并发编程理论基础
JVM并发基础
并发编程模型
在并发编程中,存在两个关键问题:线程之间如何通信、线程之间如何同步。
通信是指线程之间以何种机制来交换信息。
同步是指程序用于控制不同线程之间操作发生相对顺序的机制。
Java属于共享内存的并发模型。因此,在java中使用 共享内存中的公共状态
实现隐式通信,由程序员 手动设置
某个方法或代码片段在不同线程之间互斥执行。
Java内存模型JMM
为什么需要JMM
对于 Java 来说,你可以把 JMM 看作是 Java 定义的并发编程相关的一组规范,除了抽象了线程和主内存之间的关系之外,其还规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的。
一般来说,编程语言也可以直接复用操作系统层面的内存模型。不过,不同的操作系统内存模型不同。如果直接复用操作系统层面的内存模型,就可能会导致同样一套代码换了一个操作系统就无法执行了。Java 语言是跨平台的,它需要自己提供一套内存模型以屏蔽系统差异。
JMM解决了什么问题
在计算机系统中有三个主要部件:CPU、主存、外存(硬盘),这三个部件之间的执行速度存在很大的差异。一般情况下,程序初始存储在硬盘内,程序执行时,将需要执行的程序加载到主存中。这里主要是为了消除主存和硬盘的速度差异,将程序加载到主存中,可以极大的提高程序执行速度。同样的,CPU的执行速度同样远超主存的访存速度,如果将所有的程序数据都存储在主存中直接操作主存数据,则会导致CPU大部分时间都在等待主存的读写,导致CPU大量的性能被浪费。因此实际程序执行时,线程会将所需要的数据加载到CPU缓存中,在缓存中修改了数据值之后,再写回到主存中。
如上图,当线程1需要修改堆内存中的一个局部变量时,当CPU调度到线程1时,线程1将数据加载到CPU的高速缓存,并在其中调用系统指令修改对应数据,在完成修改后,再将数据写回到主存中。
重排序
在实际的操作系统运行环境中,程序的执行顺序并不一定是按照编码顺序执行的。在实际的执行过程中,编译器或者处理器可能会对执行进行重排序以提高指令执行效率。
编译器重排序
在不改变单线程程序执行逻辑的情况下,编译器可能会对语句执行顺序进行重排序。
假设存在 Date date = new Date();
在执行过程中,主要步骤如下:① 初始化date变量,实际可以理解为一个内存指针;② 在堆内存分配内存区域,创建Date对象;③ 将date变量指向分配的内存区域。
在实际的程序执行过程中,可能会是 ① - ③ - ②,即初始化内存变量,再分配内存区域并将变量指向这块内存区域,最后在这块区域中创建对象。
指令级重排序
现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
CPU指令重排序相关:
- 指令相关:指令相关指的是当前指令的执行依赖于前一条指令的结果。
- 数据相关:数据相关指的是当前指令需要前一条指令产生的数据才能执行。
- 控制相关:控制相关指的是当前指令的执行取决于前一条指令的跳转或分支结果。
参考计算机组成原理
JMM对指令重排序的处理
从 java 源代码到最终实际执行的指令序列,会分别经历下面三种重排序:
源代码
-编译器优化重排序
-指令级并行重排序
-内存系统重排序
-最终执行的指令序列
上述的 1 属于编译器重排序,2 和 3 属于处理器重排序。这些重排序都可能会导致多线程程序出现内存可见性问题。对于编译器,JMM 的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM 的处理器重排序规则会要求 java 编译器在生成指令序列时,插入特定类型的内存屏障
(memory barriers,intel 称之为 memory fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。
JMM 属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证
并发三要素
原子性
一个程序的执行序列,要么全部执行,要么全部不执行。概念等同于数据库事务的原子性。如代码 x++
这个操作在多线程下不具备原子性,看似只有一条语句,实际执行时需要执行以下指令:① 从内存中读取 x 的值;② 执行加一操作; ③ 将增加后的值写回到变量x。
在java中,可以使用 synchornized
关键字或者 Lock
实现原子操作。
可见性
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,即使用volatile修饰的变量的修改对其他线程是可见的。
volatile禁止重排序
volatile 关键字还禁止了一些特定的重排序,以确保程序的正确性:
- 写操作之前的读操作不会被重排序到写操作之后:即 read1; write; read2 不会被重排序为 write; read1; read2。
- 写操作之后的读操作不会被重排序到写操作之前:即 write1; read; write2 不会被重排序为 write1; write2; read。
volatile写入内存屏障
a. 内存屏障(Memory Barriers)
volatile 关键字会在读取和写入变量时插入内存屏障(Memory Barriers),确保变量的读写操作不会被重排序。内存屏障的作用是:
-
写屏障:在写操作之后插入一个写屏障,确保所有在此之前发生的写操作都已完成,并且对其他线程可见。
-
读屏障:在读操作之前插入一个读屏障,确保所有在此之后发生的读操作都能看到之前的写操作。
b. 缓存一致性
现代多核处理器中,每个核心都有自己的缓存。volatile 关键字确保了变量的读写操作会直接访问主存,而不是缓存,从而保证了变量的最新值对所有线程可见。
注意:
volatile
关键字只能保证变量的可见性,不能实现同步。
有序性
在java中,可以使用synchronized和Lock以同步锁的形式来保证有序性。