一、背景
并发编程本质问题
是:CPU、内存以及IO三者之间的速度差异。CPU速度快于内存、内存访问速度又远远快于IO
,根据木桶理论
,程序性能取决于最慢的操作,即IO操作。这样会出现CPU和内存交互时,CPU性能无法被充分利用,内存与IO交互时,内存性能也存在部分损耗,单方面提升CPU或内存的性能是无效的。为了提升CPU或内存的利用率,需要平衡三者的速度差异,计算机系统结构、操作系统以及编译程序都做出了贡献,主要体现为:
1. 计算机体系结构:为CPU增加了缓存,均衡和内存的速度差异;
2. 操作系统:增加了进程和线程,便于分时复用CPU,均衡CPU与IO设备的速度差异;
3. 编译程序:优化CPU指令的执行顺序,使得缓存被更加合理的利用。
上述方案虽然很大程度上解决了程序的性能问题,但也带来了许多隐藏的并发问题,主要为:可见性问题
、原子性问题
以及有序性问题
。
可见性问题:
定义:
一个线程对共享变量的修改,另外一个线程能够立刻看到,即可见性
导致原因:
CPU缓存
详情解析:
在多CPU时代,每个CPU都有自己的缓存,当多个CPU缓存同一份共享变量的数据时,线程A修改了共享变量,但修改目前只在CPU的缓存生效,其他CPU缓存还未来得及获取新修改的数据,线程B读取共享变量,读取的数据是老数据,存在数据不一致问题。
原子性问题:
定义:
一个或多个操作在CPU执行过程中不被中断的特性,即原子性
导致原因:
多线程上下文切换
详情解析:
多线程底层执行是按照时间片来执行的,进行任务切换时,针对的是单个CPU指令,仅能保证单个CPU指令的原子性。而高级语言的一条语句,往往是由多个CPU指令完成。例如count += 1,至少需要三条指令:
1. 首先,将变量count加载到对应CPU的寄存器中;
2. 其次,在寄存器中执行 +1 操作;
3. 最后,将结果写入到内存中
若是线程A执行完指令1后,切换到线程B来执行 count += 1操作,线程B操作后count为2,但线程A中保存的count值仍是1,导致最后的结果为2,实际上应该为3。
Java中的原子操作有哪些:
1. 除long和double之外的基本类型(int, byte, boolean, short, char, float)的赋值操作;针对long操作,直接拆分成两个32位的写入操作。
2. 所有引用reference的赋值操作;
3. java.concurrent.Atomic.*包中所有类的原子操作。
原子操作 + 原子操作 != 原子操作
【原子性对比:】
synchronized:不可中断锁,适合竞争不激烈,可读性好
Lock: 可中断,多样化同步,竞争激烈时能维持常态
Atomic: 竞争激烈时能维持常态,比Lock性能好;只能同步一个值
有序性问题:
定义:
有序性是指按照代码的先后顺序来执行,但编译器为了优化性能,可能会改变代码的执行顺序。例如:i = 1; j = 2 变成 j = 2; i = 1
导致原因:
编译优化
详情解析:
在Java领域存在一个双重检查创建单例对象的场景,创建对象JVM底层分为三个步骤:
1. 分配内存空间;
2. 在内存上初始化对象;
3. 将对象地址赋值给实例变量
此时进行了编译优化,将1,2,3变成了1,3,2。那么就会出现多线程访问时,某些线程获取的对象为null,出现空指针问题。
因此,为了解决出现的可见性、原子性以及有序性问题,Java给出了一套解决方案,即Java Memory Model,简称JMM
。
二、Java内存模型
为了解决可见性和有序性,直观上可以理解:禁用缓存和编译优化
,但程序的性能就无法保证。理想方案是:开发者按需禁用缓存和编译优化,因此Java做了两个方面的工作:
- 定义一种
抽象计算机模型
; - 定义
一系列规则
,来保证可见性和有序性。
2.1 定义
抽象计算机模型:
JMM定义了线程和主内存之间的抽象关系:线程之间共享的变量存储在主内存中,每个线程有一个私有的本地内存,每个本地内存中存储了共享变量的副本。其中本地内存[工作内存]是一个抽象概念,底层对应着缓存、寄存器以及硬件和编译器优化等。主内存和工作内存之间的规范为:
所有的共享变量都存储于主内存
:这里的变量值是实例变量、类变量以及数组,因为堆和方法区是线程共享的。(局部变量属于线程私有,不存在线程安全问题)工作内存
:每一个线程有自己的工作内存,工作内存中保留了被多个线程使用的变量的工作副本线程不能直接读写主内存中的变量
① 只能操作自己的工作内存中的变量,
② 然后再同步到主内存中工作内存的屏蔽性
:不同线程之间不能直接访问对方工作内存中的变量,线程之间的值传递需要通过主内存来完成。(可见性问题的罪魁祸首)
一系列规则:
volatile、synchronized和final三个关键字,以及六项Happens-Before规则。
2.2 规则
Happens-Before规则指的是:前面一个操作结果
对后续操作是可见的。下面为详细的规则:
单线程规则:
一个线程中的每个操作,happens-before于该线程的任意后续操作;监视器锁规则(synchronized):
对一个锁的解锁,happens-before于随后对这个锁的加锁;volatile变量规则:
对一个volatile修饰的变量的写,happens-before于随后对这个变量的读;传递性:
如果A happens-before B、 B happens-before C、则A happens-before C;线程start启动规则:
主线程A启动子线程B后,子线程B能够看到主线程在启动子线程B前的操作结果;线程join()规则
: 主线程A等待子线程B完成,当子线程B完成后,主线程能够看到子线程的操作结果。final规则
: 通过final修饰变量,告诉编译器着变量是不会发生改变的,可以尽情优化。
三、总结
上述解决了可见性和有序性问题,原子性问题通过互斥锁
可以完美解决。