参考:海子的博客Java并发编程:volatile关键字解析
三个重点:
- 原子性
- 可见性
- 有序性
讨论这三点之前先说一下计算机的内存模型:
-
CPU对程序的指令的执行速度远远大于从内存中读写数据的速度
所以如果让CPU直接访问内存来读数据再写结果去内存,就会效率非常低下。因此就有了高速缓存——Cache这个东西。
- 举个例子看看他们怎么工作的:
i=0;
单线程运行代码:i=i+1;
步骤是:Cache从主存读取i=0
, CPU从Cache读取i,CPU运算i=i+1, 将结果i=1
写入Cache,Cache再将结果i=1
写入主存。
单线程没问题,但是多线程中,比如开启两个线程分别执行i=i+1;
,我们希望结果是i=2
但是结果可能会是这样:
线程1的cache从内存读i=0,这时,没有马上计算,CPU马上换到线程2读取,这是i仍然是0,然后线程1和线程2的cache再分别把i=0给cpu进行计算都得到i=1,然后分别刷新到主存中,最后的结果是i=1
因此
出现了缓存一致性协议,意思是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
现在进入正题:
java内存模型:
Java内存模型规定所有的变量都是存在主存当中,每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。
图示:
- 原子性:
- 在Java中,对基本数据类型变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
- 只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。
x = 10; //语句1
y = x; //语句2
x++; //语句3
x = x + 1; //语句4
哪几个是原子操作?只有语句一是原子性操作;其他操作除了读取之外都有赋值,因此不是原子性。
如果要实现大范围的原子性操作,就要用同步。
- 可见性:
在CPU读写数据的时候,不同线程之间对变量的更行可能没有及时刷新到内存中去,因此其他线程不能立刻看到修改后的变量,这是不可见的。java中提供了两种实现可见性的方法:
1、volatile关键字:当一个变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
2、同步:synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。 - 有序性:
为了提高程序的运行效率,没有数据依赖性的代码段可能会被处理器重新排序,也叫指令重排序。(但是有数据依赖性的代码就不会重排序)