关于Java中的原子性、可见性的学习
当然原子性、可见性不仅限于Java的并发编程中,这三种性质的问题是在所有并发编程中普遍拥有的。
原子性
在Java中,对基本数据的读取与赋值操作是原子性的。
大家都知道原子是自然界中很基本的单位。
那什么是原子性呢?学习过数据库相关知识的人应该都知道数据库事务中有ACID四个基本要素,其中A就是我们所说的原子性(Atomicity),数据库事务中,原子性的意思就是:一个事务,要么执行,要么不执行。简单说就是不存在执行到一半的情况,一个事务就是最小的单元。
“要么有,要么无。”
首先我们需要知道: 在Java中,对基本数据的读取与赋值操作是原子性的。
比如下面的代码:
int i = 1;
把1赋值给i这个操作就是原子性的。
那就有同学要问了:这就一句代码,怎么看都是一步操作到位,这不是明摆着就是“原子性”吗?
非也非也,我们假设赋值这个int类型的值时,是先赋值给低16位,再赋值给高16位,这样一来,就成了两步操作。如果刚好赋值给低16位的时候线程断了,那么i所赋的值就不一定是你想赋的值了。
是不是马上感觉到了原子性的重要性?
这里需要注意的是Java中,自增语句不是原子性的:
i++;
实际上,自增是两个操作,首先读取i的值,然后再是赋值给i,两个步骤都是具有原子性的,但是两个原子性操作在一起就不再拥有原子性。
正确的方法可以使用java.util.concurrent.atomic包中的比如AtomicInteger类,它可以让赋值读取具有原子性。
可见性
在Java中,解决可见性问题的方法就是在你所需多线程访问的变量在添加volatile关键词。
我们试想一下,当CPU总是去访问物理内存去获取变量,然后频繁地去修改物理内存上的值,是不是太麻烦了?这将导致CPU花大部分的时间在获取和修改物理内存的值。
所以,现在的CPU内部普遍它自己的内存空间,我们称之为 “CPU缓存” 。
那CPU缓存有什么作用?百度百科中这样描述:
CPU缓存(Cache Memory)是位于CPU与内存之间的临时存储器,它的容量比内存小的多但是交换速度却比内存要快得多。高速缓存的出现主要是为了解决CPU运算速度与内存读写速度不匹配的矛盾,因为CPU运算速度要比内存读写速度快很多,这样会使CPU花费很长时间等待数据到来或把数据写入内存。在缓存中的数据是内存中的一小部分,但这一小部分是短时间内CPU即将访问的,当CPU调用大量数据时,就可先缓存中调用,从而加快读取速度。
一句话说就是,让CPU处理指令更快更快更快更快更快更快更快更快。
**但是与之而来的就是并发编程在多核CPU中的可见性问题。**因为现在的电脑都是多核处理器,也就是一个CPU内部是由多个CPU组合而成。
不妨我们做一个这样的假设:
// Thread1int i = 1;i = 10;// Thread2int j = i;
我们假定线程1在CPU1中执行,线程2在CPU2中执行。
具体流程如下:
-
首先线程1定义了i并将1赋值给了i,此时i被载入到主存(也就是物理上的内存)。
-
然后线程1执行“i=10”语句,CPU1会将修改后的i放入CPU缓存中,注意:并没有直接载入到主存。
-
线程2中先获取i的值,CPU2缓存中没有i,所以就去获取主存中的i值,然而主存中还是i=1,这样,j就被赋值为1。
流程结束。现在我们应该知道了什么是可见性问题,并能感受到问题的严重性。
**事实上,很难模拟出这样的结果。**因为我们无法让某个线程指定某个特定CPU,这是系统底层的算法,我想JVM应该也是没法控制的。还有最重要的一点,就是你无法预测CPU缓存何时会将值传给主存,可能这个时间间隔非常短,短到你无法观察到。还有就是线程的执行的顺序问题,因为多线程你无法控制哪个线程的某句代码会在另一个线程的某句代码后面马上执行。
那么如何才能避免上述情况?
Java提供volatile关键词,只要将这个关键词修饰在你考虑会出现这个问题的变量声明前,就可以避免可见性问题。
volatile int i = 1;
当添加该关键词后,JVM会得知这个变量需要确保在应用中的可见性,然后通知系统:”这个变量直接在主存读取修改,别放到CPU缓存里了“。
“yes,sir!”
然后就不需要考虑可见性问题了。
当然解决可见性问题还有一招更灵,就是让这个域完全由synchronized方法或代码块来维护,那就不必在将其设置为volatile了。 因为同步会导致向主存中刷新。
注意:当一个域的值依赖于它之前的值时(例如递增一个计数器),volatile就无法工作了。