多线程开发带来的并发问题大致有以下3类:
1.可见性问题
2.原子性问题
3.有序性问题
下面分别对这3类问题的产生根源和解决之道一一讲解:
1.可见性问题
简而言之,就是一个线程对另一个线程的操作结果能够立刻可见。
问题根源:
在多核时代,同一进程的多个线程可能位于不同CPU之上,而不同CPU各自拥有自己独立的缓存,但是会共用同一块主存。在实际运算时,CPU运算并不是直接操作主存地址上的变量的,而是读取主存变量到各自的缓存(即对于同一共享变量每个CPU都有各自的副本)。其中一个CPU上的线程不会主动将运行结果写回到主存,这样另一个CPU的线程读取主存变量的值还是没变,这就造成这个线程看不到另一个线程对同一个共享变量的操作变更。
解决思路:
确保CPU对共享变量的操作及时写到主存,同时触发位于其他CPU的线程重新从主存加载新的取值
具体做法:
在java中,对共享变量使用volatile修饰
2.原子性问题
问题根源:
这个源于高级语言单条执行语句对应计算机一条执行指令的误解。实际上即使是一个自增操作对应到CPU指令有3条(读取初始值,CPU加1运算,写回运算结果值)。单个CPU指令运行是原子性的,不会被线程切换中断,但是高级语言的一条语句却对应多条CPU指令,就有可能被线程切换中断,导致彼线程来不及不知道此线程的运行结果的变化。
解决思路:
确保需要一次性执行的指定不被线程切换的中断指令打断,要被全部执行,要么全部不执行
具体做法:
在java中,对指令所在的代码块或者方法加锁
3.有序性问题
问题根源:
有序性问题源于Java编译器对指令执行顺序调整来优化执行性能导致的指令有序性在多线程情况下遭到破坏的情况。特别是线程1需要依赖线程2对共享变量A的变更情况来决定是否对共享变量B进行读取处理的情形有影响。本来从源代码来看,线程2已经先对变量B作了写入变更,然后也对起到标记作用的共享变量A作了变更,这是线程1看到共享变量A已经变了,就能确定共享变量B的值肯定也变了,貌似没什么问题。但是由于编译器的优化作用,导致线程1对变量A的变更早于变量B,这时线程2看到变量A已经变了,但是变量B实际上还没来得及变(线程切换导致),所以线程2这时就产生了误解,以为B已经变了,这就对线程2接下来的操作起到了误导。
解决思路:
禁用编译器对这块代码的重排序优化
具体做法:
final禁用对象初始化与赋值操作重排序,即确保新创建的对象在构造函数初始化完成后完成赋值给共享变量的操作。
volatile修饰共享变量,确保编译后指令像源代码指定的那样,读变量操作在写变量操作之后,不受重排序影响(jdk1.5增强)。