原子性、有序性、可见性
在并发编程中,我们通常会遇到以下三个问题:原子性问题,可见性问题,有序性问题。我们先看具体看一下这三个概念:
1. 原子性
原子性:即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
一个很经典的例子就是银行账户转账问题:
比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。
试想如果这个操作不具备原子性,会造成什么样的后果。假如从账户A减去1000元之后,操作突然中止。然后又从账户B取出了500元,取出500元之后,再执行往账户B添加1000元的操作。
不具备原子性的结果就是,导致账户A虽然减去了1000元,但是账户B没有收到这个转过来的1000元。所以这个操作必须要具备原子性才能保证不出现一些意外的问题。
2. 可见性
可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
举一个简单的例子:
线程1
boolean flag = false;
flag = true;
线程2
boolean signal;
signal = flag;
假设线程先开始执行,获取flag的值为false存入线程1 的缓存中,存入主存,之后又将flag赋为true,再将true更改到线程1 的缓存中,并没有立即存入主存中;
线程2开始执行signal = flag,会先去主存中读取flag的值,并加载到线程2 的缓存中,此时signal读到的flag的值为false而不是true。这就是可见性,线程1对变量做了修改,线程2 没有立即看到修改后的值。
3.有序性
有序性:即程序执行的顺序按照代码的先后顺序执行。(串行)
举例:
int i = 0;
boolean flag = false;
i = 1; //语句1
flag = true; //语句2
上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?不一定,为什么呢?
这里可能会发生指令重排序(Instruction Reorder)。
什么是指令重排序??
一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。
但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢?再看下面一个例子:
int a = 10; //语句1
int b = 5; //语句2
a = a + 10; //语句3
b = a * a; //语句4
这段代码的执行顺序是:语句1====》语句2====》语句3 ====》 语句4
但是有没有可能执行顺序是这样的:2-----1-------4-------3
答案是不会的,处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令2必须用到1的结果,那么处理器会保证1会在2之前执行的。
指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。