多线程并发出现的背景
CPU、内存、IO设备三者速度存在数量级差异,为了平衡速度差异,更高效利用CPU,操作系统、编译器都做了优化,主要体现为:
- CPU增加了缓存,以均衡与内存的速度差异
- 操作系统增加了进程、线程,以分时复用CPU,进而均衡CPU与IO设备的速度差异
- 编译器优化指令执行次序,使得缓存能够更加合理地使用。
在享受这些特性的同时,并发编程也随之带来了一些问题,主要有
-
缓存导致的可见性问题
一个线程对共享变量的修改,另一个线程能够立刻看到,成为可见性
在多核时代,每个CPU都有自己的缓存,当多个线程在不同的CPU上执行时,这些线程操作的是不同CPU的缓存,这就会出现一个线程对一个共享变量的操作对另一个线程不具备可见性。
-
线程切换带来的原子性问题
一个或多个操作在CPU执行过程中不被中断的特性成为原子性
由于IO太慢,早起的操作系统引进了多进程概念,操作系统将执行时间分成多个时间片,每个进程交替得到时间片被执行,对用户来说,表现出操作系统能“并行”执行任务。比如当一个进程正在执行IO操作时,将自己标记为“休眠“状态并让出CPU执行权,这样CPU就可可以在等待IO的过程中执行其他进程的指令。
早起的操作系统是基于进程来调度的,不同进程空间不共享内存空间,所以进程要做任务切换就要切换内存映射地址,而一个进程创建的所有的线程是共享一个内存空间的,所以线程做任务切换成本就很低了。现代操作系统都基于更轻量的线程来调度,现在我们提到的”任务切换“都是指”线程切换“。
操作系统做任务切换是基于CPU指令级的切换,即当执行完某个CPU指令后,时间片耗完就切换,然而高级语言的一条语句可能需要多个CPU指令才能完成,比如i++,这条语句从语言层面看来应该是一个不可分割的主体,一旦执行肯定会被执行完毕。但这条语句需要多条CPU指令,当两个线程并发执行这条语句的时候,我们预期是增加2,但实际上会出现只增加了1的情况。其原因如下:
线程1获得CPU执行权,进行内存加载,计算+1操作,但还未将新结果写回内存时,线程1的时间片耗完,线程2被调度开始执行,同线程1的执行流程,先拿到内存中的变量值(未增加),执行完+1操作,再写回内存。此时线程1又重新被调度执行,直接将当时加1后的结果写入内存。
-
编译优化带来的有序性问题
编译器在保证单线程时计算结果不改变的前提下,为了更充分利用CPU缓存(具体什么原理待了解?),可能会重排部分指令的顺序。但是这种优化策略在多线程环境下,就带来了不确定性。举个例子?双重检查单例模式
明确了,在享受快的同时,带来了并发环境下的三个问题,分别是可见性、原子性和有序性。在Java中,是怎么解决这三个问题的呢?
网友评论