
并发编程世界里,由于CPU缓存导致的可见性问题,线程切换导致的原子性问题,以及编译器重排序导致的有序性问题是并发编程Bug的根源。
原子性是指一个操作是不可中断的,即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他的线程干扰。 一般认为CPU的指令都是原子操作,但是我们写的代码就不一定是原子操作了。一条高级语句在CPU中可能会分成若干条指令来执行,每条指令执行完之后就有可能会发生线程切换。故线程切换造成的原子性问题。
例如:count=count+1 共有三个指令
操作系统做任务切换可以发生在任何一条CPU指令执行完,是CPU指令执行完。
在并发时,由于编译器重排序导致不是按照程序的顺序执行。 例如:
public class SingletonDemo {
private static SingletonDemo instance = null;
private SingletonDemo(){
}
static SingletonDemo getSingletonDemo() {
if (instance == null) {
synchronized (SingletonDemo.class) {
if (instance == null) {
instance = new SingletonDemo(); //6
}
}
}
return instance;
}
}
这里会有有序性问题:
问题主要出在了 new SingletonDemo() 这一步
因为instance = new SingletonDemo();主要有三个指令
一个线程对共享变量的修改。另外一个线程能够立刻看到,我们称之为可见性。 可见性问题可能在各个环节产生,比如:前面提到的指令重排序产生的可见性问题,另外在编译器的优化或者某些硬件的优化都会产生可见性问题。 比如:某个线程将一个共享值优化到了内存中,而另一个线程将这个共享值优化到了缓存汇总,当修改内存中的是值的时候,缓存中的值是不知道这个修改的。 比如有些硬件的优化,程序在怼同一个地址进行多次写是,它认为是没有必要的,最保留最后一次的写,那么之前写的数据对其他线程就不可见了。 如图所示:

在这里插入图片描述 共享变量V可以由线程A和线程B同时操作,并写入各自的CPU缓存,然后由CPU的寄存器写入内存中。
public class LongTest {
private static long atest = 0L;
public void countTest() {
for (int i = 0; i < 10000; i++) {
atest = atest + 1;
}
}
public static void main(String[] args) throws InterruptedException {
final LongTest longTest = new LongTest();
Thread threadA = new Thread(new Runnable() {
public void run() {
longTest.countTest();
}
});
Thread threadB = new Thread(new Runnable() {
public void run() {
longTest.countTest();
}
});
threadA.start();
threadB.start();
threadA.join();
threadB.join();
System.out.println("*******获得到的atest值为=" + atest);
}
}
如上程序,运行之后我们会发现 atest 值永远都不会到达20000,而是在10000-20000之间的随机数。 原因分析:假设线程A和线程B同时执行 atest = atest + 1;, 线程A 读取到的原值是0,执行+1操作之后,得到新值1,同样的,线程B也是读取到的原值0,然后执行+1操作,得到新值1。这样就永远得不到结果2。 类推的话,循环执行10000次也是同理,线程A执行+1操作时不能及时获得线程B已经写入的值,故导致值永远不可能达到20000。
并发编程中主要的问题就是可见性问题, 原子性问题,有序性问题。本文介绍了这三种问题的发生原因,以及发生的场景。