并发编程要解决的主要问题
可见性
可见性问题出现的原因是现在CPU基本都是多核CPU,每个CPU都会有自己的L1、L2缓存,如下图所示:
CPU缓存是为了平衡CPU寄存器和内存之间的速度差异,下面这张图学计算机组成原理的时候应该都看过。
在多核时代,每颗CPU都有自己缓存,这时CPU缓存和内存的数据一致性就没那么容易解决了。当多个CPU在不同的CPU上执行时,这些线程操作的是不同的CPU缓存,这时一个线程对变量的操作对于另外一个线程就不具备可见性了。
我们刚开始学多线程编程时,都看过多个线程对同一个成员变量不加锁的操作,比如两个线程都对count值加1000次,最终的结果可能是比2000少,这就是由于可见性问题导致的。
针对内存可见性,底层专门有一个协议,MESI高速缓存一致性协议,来协调缓存之间的一致性。
原子性
在Java中,我们都知道用synchronized来保证操作的原子性,因为Java并发编程时基于多线程的,多线程就会涉及到任务切换。高级语言里的一条语句往往需要多条CPU指令来完成,例如count += 1,至少需要三条CPU指令:
指令1:首先,需要把变量count从内存加载到CPU寄存器
指令2:然后,在寄存器中执行+1操作
指令3:最后,将结果写入内存(缓存机制导致可能写入的是CPU缓存而不是内存)
CPU能保证的原子操作是CPU指令级别的,而不是高级语言的操作符。因此,很多时候我们需要在高级语言层面保证操作的原子性。
有序性
有序性,顾名思义,有序性指的是程序安装代码的先后顺序执行。单编译器为了优化性能,有时候会改变程序中语句的先后顺序。
在C语言中,gcc默认提供了5级优化选项的集合:
-O0:无优化(默认)
-O和-O1:使用能减少目标文件大小以及执行时间并且不会使编译时间明显增加的优化.在编译大型程序的时候会显著增加编译时内存的使用.
-O2: 包含-O1的优化并增加了不需要在目标文件大小和执行速度上进行折衷的优化.编译器不执行循环展开以及函数内联.此选项将增加编译时间和目标文件的执行性能.-Os:专门优化目标文件大小,执行所有的不增加目标文件大小的-O2优化选项.并且执行专门减小目标文件大小的优化选项.
-O3: 打开所有-O2的优化选项并且增加 -finline-functions, -funswitch-loops,-fpredictive-commoning, -fgcse-after-reload and -ftree-vectorize优化选项.
不同的编译方式,有的侧重优化binary文件的size,有的侧重优化speed。但很可能优化后int a=3;b=2;就变成了b=2;a=3。
针对以上问题,Java提供的解决方式如下图所示: