前言
缓存导致的不可见性问题,编译优化带来的乱序性问题,线程切换带来的非原子性问题。其实缓存、编译优化、线程切换的目的和我们写并发程序的目的是相同的,都是为了提高程序性能和安全性。但是技术在解决一个问题的同时,必然会带来另外一个问题,所以在采用一项技术的时候,一定要清楚它带来的问题是什么,如何去规避。
不可见性问题
一个线程对共享变量的修改,另一个线程能够立刻看到。对于如今的多核处理器,每个CPU内核都有自己的缓存,而缓存仅对它所在的处理器内核可见,CPU缓存与内存的数据不容易保证一致。为了避免处理器停顿下来等待向内存写入数据而产生的延迟,处理器使用写缓冲区来临时保存向内存写入的数据。写缓冲区合并对同一内存地址的多次写,并以批处理的方式刷新,也就是说写缓冲区不会即时将数据刷新到主内存中。缓存不能及时刷新导致了不可见性问题。
Java内存模型设计有主内存和工作内存,模型规定了所有的变量都存储在主内存中,每条线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。这里的工作内存是JMM的一个抽象概念,也叫本地内存,其存储了该线程读/写共享变量的副本。就像每个处理器内核拥有私有的高速缓存,JMM中每个线程拥有私有的本地内存。
总结来说,线程不能直接对主内存中的数据进行操作,必须将主内存中的数据加载到工作内存中,这样在多核CPU下就会产生不可见性,而我们的目标就是要做到可见性。volatile可以解决不可见性,volatile修饰的变量,当它被修改时,值会立即更新到主存,其他线程可以及时在内存中读取到最新值。
乱序性问题
指令在执行过程中,为了优化性能,有时候会改变程序中语句的先后顺序,这样的改变可能会影响程序整个运行的结果。CPU在读等待同时指令执行是CPU乱序执行的根源,读指令时可以同时执行不影响的其他指令。被volatile修饰的变量,会禁止指令重排序,从而保证有序性。
非原子性问题
原子的意思代表着“不可分”,一个或多个操作在CPU执行的过程中要么执行都成功,要么执行都失败,不可中断,不存在中间状态,我们称之为原子性。原子性是拒绝多线程交叉操作的,不论多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。CPU能保证的原子操作是CPU指令级别的,而不是高级语言的操作符。
线程切换导致了原子性问题,Java并发程序都是基于多线程的,自然也会涉及到任务切换,任务切换的时机大多数是在时间片结束的时候。我们现在基本都使用高级语言编程,高级语言里一条语句往往需要多条CPU指令完成。如 count++,至少需要三条CPU指令。
指令1:首先,需要把变量 count 从内存加载到工作内存;
指令2:之后,在工作内存执行 +1 操作;
指令3:最后,将结果写入内存。
两个线程A和B同时执行 count++,即便 count 使用 volatile 修饰,我们预期的值是2,但实际可能是1。