并发编程中存在的问题
在并发编程中,我们常常会遇到如下三个问题:
- 原子性问题
- 可见性问题
- 有序性问题
下面让我们先来了解一下这三个问题的基本概念。
1、原子性: 一个操作或者一组操作(即一段代码)要么全部执行,并且执行过程中不会被其他因素打断,要么全部不执行。
原子性的问题我们在MySQL的事务的四大特性里提到过,这里的概念跟MySQL的几乎相同。既然如此,我们就还拿常用的银行转账的实例来说明原子性的问题。
假如A有500元,B有500元,A要向B转账200元,该怎么做呢?当然要分成两步:
(1)从A的账户上扣除200元
(2)在B的账户上增加200元
但是如果A转完帐之后银行系统出现了故障,B的账户上并没有收到钱,即A的账户扣除了钱,B的账户却没有多钱。此时就出现了原子性的问题。
2、可见性: 即一个线程修修改了某个共享变量的值,那么其他线程应当能立即看到此变量被修改后的值。
举个例子,看如下代码:
// 线程1的执行体
int i = 1;
i = 2;
// 线程2的执行体
int j = i;
两个线程如果同时在不同的CPU上执行,假设线程1在CPU1上执行,线程2在CUP2上执行。因为线程是并发的,当线程1执行完语句2时,CPU1将i的值加载到高速缓存中,赋值完毕再放入高速缓存中,此时变量i的值并没有立即写入主存,即 i 的值还未改变。此时线程2执行,CPU2先读取 i 的值放入高速缓冲区,此时因为线程1的修改还未写入主存,所以变量 i 的值并未变为2,而是赋值前的1,因此此时的 j = 1。
这就是我们所说的可见性问题了,线程1修改的值没有立即让线程2看到,导致出现程序问题。
3、有序性: 即程序按照代码的先后顺序执行。
同样举个例子来说明:
int a = 1;
boolean isGood = ture;
a = 3; // 代码1
isGood = false; // 代码2
从语句上看,代码1是在代码2之前的,但是实际执行的时候,代码1一定在代码2之后么?不一定,这里会发生指令重排序。处理器为了提高程序执行的效率从而对执行语句进行优化,即不保证指令执行顺序和代码顺序相同,但是重排序有一个前提,那就是重排序后程序的运行结果要和原程序(即未进行重排序)的执行结果相同,保证程序的正确性。再看如下代码:
int a = 1; // 语句1
int b = 5; // 语句2
b = a + 1; // 语句3
a = a + 1; // 语句4
如果JVM虚拟机对如上代码进行重排列,那么代码顺序可能的是:语句2→语句1→语句3→语句4
那有的同学可能要问了,顺序 语句2→语句1→语句4→语句3 不可以么?当然不可以,因为它违背了JVM重排序的前提。
处理机在进行指令重排序的时候要考虑到指令之间的依赖关系,如果指令1需要用到指令2的结果,那么指令2一定在指令1之前执行。
以上是单线程的情况,多线程会如何呢?看如下例子:
// 全局变量Student
//线程1
student = Student.init(); // 对Student对象进行初始化 语句1
flag = true; // 语句2
//线程2
while(flag){
sleep();
}
addStudent(student);
线程1中的两行代码并没有依赖关系,因此他们可能会被重排列。如果 先修改flag的值为 ture 而 student 并未初始化时(语句2先执行完毕,语句1还未执行),线程2启动,则不会阻塞而直接调用 addStudent(student) 方法,但此时的 student 为空,这就导致程序出现了问题。
由此我们可以的出结论,指令重排序不会影响单个线程的执行正确性,但会影响到并发线程运行的正确性。
综上所述,我们必须同时保证程序的原子性,可见性和有序性才能保证并发程序的正确性,三者若有一个保证不了,那么程序就有可能出现错误。