为什么会有这个关键字?
计算机在执行程序时,每条指令都是在CPU
中执行的,而执行指令过程中,会涉及到数据的读取和写入,但由于CPU
执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU
执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU
里面就有了高速缓存。
也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU
的高速缓存当中,那么CPU
进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。这样的操作在单线程中不会出现什么问题,但是在多线程中运行就可能会出现问题,会出现缓存不一致问题。所以在多线程的时候就需要加锁了,我们知道的在CourrentHashMap
中是通过分段锁和CAS
来实现线程安全的,这里面就涉及到了Volatile
关键字。
并发编程的的三个概念(特性)
原子性
原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
一个很经典的例子就是银行账户转账问题:
比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。
试想一下,如果这2个操作不具备原子性,会造成什么样的后果。假如从账户A减去1000元之后,操作突然中止。然后又从B取出了500元,取出500元之后,再执行 往账户B加上1000元 的操作。这样就会导致账户A虽然减去了1000元,但是账户B没有收到这个转过来的1000元。所以这2个操作必须要具备原子性才能保证不出现一些意外的问题。
可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
1 | //线程1执行的代码 |
假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =10
这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。此时线程2执行j = i
,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10.
这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。
有序性
有序性:即程序执行的顺序按照代码的先后顺序执行。
1 | int i = 0; |
一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。所以JVM
在真正执行这段代码的时候不会保证语句1一定会在语句2前面执行。
指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。
volatile关键字的两层语义
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile
修饰之后,那么就具备了两层语义:
- 保证了不同线程对这个变量进行操作时的内存可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
- 禁止进行指令重排序。
它能保证程序的可见性,一定程度的有序性,但是不能保证原子性(可以采用synchronized
)。
使用volatile必须具备以下2个条件:
- 对变量的写操作不依赖于当前值
- 该变量没有包含在具有其他变量的不变式中
Volatile底层原理
如果把加入volatile
关键字的代码和未加入volatile
关键字的代码都生成汇编代码,会发现加入volatile
关键字的代码会多出一个lock
前缀指令。
lock
前缀指令实际相当于一个内存屏障(由于现代操作系统都是多处理器操作系统,每个处理器都会有自己的缓存,可能存再不同处理器缓存不一致的问题,而且由于操作系统可能存在重排序,导致读取到错误的数据,因此,操作系统提供了一些内存屏障以解决这种问题),它提供以下功能:
- 重排序时不能把后面的指令重排序到内存屏障之前的位置
- 使得本
CPU
的Cache
写入内存 - 写入动作也会引起别的
CPU
或者别的内核无效化其Cache
,相当于让新写入的值对别的线程可见。
这样的话两个线程改变内容的时候就是这样的
写两条线程Thread-A
与Threab-B
同时操作主存中的一个volatile
变量i时,Thread-A
写了变量i
,那么:
Thread-A
发出LOCK#
指令- 发出的
LOCK#
指令锁总线(或锁缓存行),同时让Thread-B
高速缓存中的缓存行内容失效 Thread-A
向主存回写最新修改的i
Thread-B
读取变量i,那么:
Thread-B
发现对应地址的缓存行被锁了,等待锁的释放,缓存一致性协议会保证它读取到最新的值
由此可以看出,volatile
关键字的读和普通变量的读取相比基本没差别,差别主要还是在变量的写操作上。
手写一个计数器,开10个线程,保证最后计数输出为10
1 | public class Test { |