多线程编程核心技术(二)并发编程BUG的源头
并发编程BUG的源头,如果深耕一点的话就是硬件上带来的差异性,CPU,内存,I/O之间的速度存在着差异。CPU:内存:I/O=100000:1000:1。由于CPU的执行太纯粹导致过快,而I/O实在不够纯粹。根据水桶效应其实可以很明显的感觉出整体的数据是取决最短的那块木板。
目前针对这些速度差,有了不少的解决方法
1.CPU增加了一二三级高速缓存,减缓了和内存之间的速度差————>中间层思想
2.操作系统增加了进程,线程以分时复用CPU,进而均衡CPU和I/O时间的速度差————>整合思想
3.编译程序优化指令执行次序,使得缓存能够得到更加合理地利用
问题:缓存导致的可见性问题
可见性指:一个线程对共享变量进行修改,另外一个线程访问这个变量的时候可以得到修改后的最新值。问题的来源就是现在的多核CPU都有自己的高速分级缓存L1,L2,L3
public class Test { private static long count = 0; private void add10K() { int idx = 0; while(idx++ < 10000) { count += 1; } } public long calc() throws InterruptedException { final Test test = new Test(); // 创建两个线程,执行add()操作 Thread th1 = new Thread(()->{ test.add10K(); }); Thread th2 = new Thread(()->{ test.add10K(); }); // 启动两个线程 th1.start(); th2.start(); // 等待两个线程执行结束 th1.join(); th2.join(); return count; } public static void main(String[] args) throws InterruptedException { Test test = new Test(); System.out.println(test.calc()); } }
由于无法时时刻刻的对主内存进行刷新所以造成了这种可见性的问题。、
问题:线程切换造成的原子性问题
CPU支持线程间进行切换,这样的话就可以一边听歌一边写代码了。CPU由于切换很快,所以在使用的时候没有感受到明显的卡顿感。
如果服务器进去IO操作,理想情况就是IO读到内存的这个过程中,CPU让出时间给别的程序进行执行,在读到内存之后再归还对应的资源来进行下面的指令操作。
听起来好像很简单,不过在之前实现起来还是有难度的,Unix解决了这个问题。早期的操作系统基于进程来调度CPU,不同进程之间不会共享内存的,切换进程需要修改内存的映射。而现在的线程则是共享内存的,所有切换成本相对低。现在说的任务切换一般都是线程切换。
但是为什么i++这种会导致不安全呢。i++其实是三步进行的,读,写,放(可能放回的是CPU缓存而不是内存,就导致读的时候出现可见性问题)
i++并不是一个原子性的操作,我们把一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性。
问题:编译优化带来的有序性问题
java代码写完进入编译的时候是会进行优化的。这边就会可能出现Bug
例如一个双重check的单例
public class Singleton { static Singleton instance; static Singleton getInstance(){ if (instance == null) { synchronized(Singleton.class) { if (instance == null) instance = new Singleton(); } } return instance; } }
JVM会自然的进行优化,这就是导致两个线程同时去进行单例获取的时候,A进行new Singleton操作,B去的时候直接拿到了,但其实A在进行New的时候 是没有经过初始化的,那么就可能导致B拿到的会出现空指针。