天天看点

并发编程bug的源头,可见性,原子性,有序性

可见性:

  在单核时代,所有的线程都是在一颗cpu上执行的,操作的都是同一个CPU的缓存,一个线程的写,对宁一个线程一定是可见的。在多核时代,每颗cpu都有自己的缓存,当多个线程在不同的CPU上执行的时候,线程A操作的是CPU1上的缓存,线程B操作的是CPU2上的缓存,在操作共享变量的时候,A的操作对B是不具备可见性的,例如两个线程同时操作变量int a =1,分别执行a+1操作,当两个线程同时读取a=1,到自己的缓存,进行加1操作,同时再写入内存,我们发现内存中的值是2,而不是我们期望的3,这就是缓存的可见性问题

原子性:

   我们把一个或多个操作在CPU执行的过程中不被中断的特性称为原子性。当多线程在并行执行的时候,其实是一个线程执行一段时间之后,另外一个线程在执行一段时间,由于中间时间切换的时间很短,就以为两个线程是并行执行的,我们称这种为任务切换,切换的时间,例如50毫秒,称为“时间片“.

   在java语言中,高级语言的一条语句往往需要多条cpu指令来完成,例如a+=1,就需要分3条cpu指令完成

   1.把a从内存加载到cpu的寄存器

    2.在寄存器中进行a+1操作

    3.结果写回内存

    在多线程执行的时候,例如a=1,当线程把a读到寄存器之后,任务切换了,这时候线程b也把a=1,读到了自己的寄存器里,两个线程执行的结果就是2,而不是期望的3,这就导致bug

  有序性

    编译器为了优化性能,往往会改成程序的执行顺序,例如a=7,a=8,优化之后会变成a=8,a=7,调整了语句的顺序,但不会影响最终结果。但在多线程下编译器优化会导致bug

  例如,在java中的双重检查创建单例对象的时候

public static Singleton getInstanceDC() {
 2     if (_instance == null) {                // Single Checked
 3         synchronized (Singleton.class) {
 4             if (_instance == null) {        // Double checked
 5                 _instance = new Singleton();
 6             }
 7         }
 8     }
 9     return _instance;
10 }      

当执行_instance = new Singleton() 这句话的时候由于编译器优化

执行顺序变成 1.分配一块内存M  2.将M的地址赋值给Singleton对象 3.在内存M上初始化对象

单我们以为的顺序应该是 1.分配一块内存M  2.在内存M上初始化对象 3.将M的地址赋值给Singleton对象 

编译器优化之后,当多线程执行上面的代码的时候,线程A执行到new  Singleton(),在第二部的时候进行线程切换,线程B开始执行,线程B执行到第一个_instance == null的时候,不为null了,所以就直接返回了instance,而此时instance是没有初始化的,当访问instance的成员变量的时候就会报空指针异常。这就是编译器优化重排序,在多线程环境下出的bug

继续阅读