引入
“如果向一个变量写入值,这个变量可能会被另一个线程读取,或者在从另一个变量读值,而这个变量可能之前被另一个线程写入的。所以必须需要同步。”
有的时候为了读写一个或两个实例域就使用同步,显得开销过大。如果一个变量使用volatile,则它比使用synchronized的成本更加低。因为它不会引起线程上下文的切换和调度。
我们先从操作系统角度分析volatile:
- 计算机在运行程序时,每条指令都是在CPU中执行的,在执行过程中势必会涉及到数据的读写。我们知道程序运行的数据是存储在主存中,这时就会有一个问题,读写主存中的数据没有CPU中执行指令的速度快,如果任何的交互都需要与主存打交道则会大大影响效率,所以就有了CPU高速缓存。CPU高速缓存为某个CPU独有,只与在该CPU运行的线程有关。
- 有了CPU高速缓存虽然解决了效率问题,但是它会带来一个新的问题:数据一致性。在程序运行中,会将运行所需要的数据复制一份到CPU高速缓存中,在进行运算时CPU不再也主存打交道,而是直接从高速缓存中读写数据,只有当运行结束后才会将数据刷新到主存中。
- 针对数据一致性,我们据一个例子,i++操作,当线程运行这段代码时,首先会从主存中读取i( i = 1),然后复制一份到CPU高速缓存中,然后CPU执行 + 1 (2)的操作,然后将数据(2)写入到告诉缓存中,最后刷新到主存中。其实这样做在单线程中是没有问题的,有问题的是在多线程中。如下:
假如有两个线程A、B都执行这个操作(i++),按照我们正常的逻辑思维主存中的i值应该=3,但事实是这样么?分析如下:
两个线程从主存中读取i的值(1)到各自的高速缓存中,然后线程A执行+1操作并将结果写入高速缓存中,最后写入主存中,此时主存i==2,线程B做同样的操作,主存中的i仍然=2。所以最终结果为2并不是3。 - 如果要解决上述多说的数据一致性,有两种方案:
- 通过在总线加LOCK#锁的方式:它是采用一种独占的方式来实现的,即总线加LOCK#锁的话,只能有一个CPU能够运行,其他CPU都得阻塞,效率较为低下。
- 缓存一致性协议,它确保每个缓存中使用的共享变量的副本是一致的。其核心思想如下:当某个CPU在写数据时,如果发现操作的变量是共享变量,则会通知其他CPU告知该变量的缓存行是无效的,因此其他CPU在读取该变量时,发现其无效会重新从主存中加载数据。 (也就是说直接从主存读取数据)
从并发的三个概念角度分析volatile:
首先先分析这三个概念:
原子性: 多个线程执行一个操作时,其中任何一个线程要么完全执行完此操作,要么没有执行此操作的任何步骤,那么这个操作就是原子的。在多线程环境下,可以通过synchronized 锁机制来保证原子性。
注意volatile不是原子性的。
可见性:可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
voliatile是可见性的
当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,当其他线程读取共享变量时,它会直接从主内存中读取。当然,synchronize和锁都可以保证可见性。
有序性:即程序执行的顺序按照代码的先后顺序执行。在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序它不会影响单线程的运行结果,但是对多线程会有影响。
volatile禁止指令重排序
Java提供volatile来保证一定的有序性
从java内存模型角度分析volitile
我们将 Java 内存模型中的主内存类比为 RAM(cpu主存),工作内存类比为 CPU的高速缓存。工作内存并非独立存在的一段内存空间,它是对CPU的寄存器、高速缓存及其他硬件的抽象描述。