文档

java体系技术文档


volatile关键字

<h2>volatile特性</h2> <ul> <li>保证了不同线程对该变量操作的内存可见性;</li> <li>禁止指令重排序;</li> </ul> <h2>Java内存模型(JMM)</h2> <pre><code>Java虚拟机规范试图定义一种Java内存模型(JMM),来屏蔽掉各种硬件和操作系统的内存访问差异,让Java程序在各种平台上都能达到一致的内存访问效果。 Java内存模型定义了程序中各个'变量'的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量的底层细节。这些变量包括实例字段,静态字段,构成数组对象的元素;但不包括局部变量和方法参数。因为后者是线程私有的,不会被共享,自然就不会存在竞争问题。 Java内存模型规定所有的变量都存储在主内存(Main Memory)中。每条线程还有自己的工作内存(Working Memory),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。</code></pre> <p>线程、主内存、工作内存三者的交互关系如图所示。 <img src="https://s2.ax1x.com/2019/12/23/lpjoOP.png" alt="lpjoOP.png" /></p> <h2>内存间的交互操作</h2> <p>主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,Java内存模型中定义了以下8种操作来完成,虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许有例外)。</p> <ul> <li>lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。</li> <li>unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。</li> <li>read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。</li> <li>load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。</li> <li>use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。</li> <li>assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。</li> <li>store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。</li> <li>write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。</li> </ul> <p>除此之外,Java内存模型还规定了在执行上述8种基本操作时必须满足如下规则:</p> <ul> <li>不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现。</li> <li>不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。</li> <li>不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。</li> <li>一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说,就是对一个变量实施use、store操作之前,必须先执行过了assign和load操作。</li> <li>一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。</li> <li>如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。</li> <li>如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定住的变量。</li> <li>对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。</li> </ul> <h2>JMM特性</h2> <ul> <li>原子性:一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉。大致认为基本数据类型的访问读写具备原子性(例外就是long和double的非原子性协定)</li> <li>可见性:可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。</li> <li>有序性:一个线程中的所有操作必须按照程序的顺序来执行。</li> </ul> <h2>指令重排序</h2> <p>指令重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。重排序保证在单线程下不会改变执行结果(两个操作间没有依赖性),但在多线程下可能会改变执行结果。</p> <h2>先行发生原则</h2> <pre><code>先行发生是Java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到。 先行发生原则是判断数据是否存在竞争、线程是否安全的主要依据。</code></pre> <p>Java内存模型中'天然的'先行发生关系:</p> <ul> <li>程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。</li> <li>管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序。</li> <li>volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。</li> <li>线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。</li> <li>线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。</li> <li>线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。</li> <li>对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。</li> <li>传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。</li> </ul> <p>结论:时间先后顺序与先行发生原则之间基本没有太大的关系,所以我们衡量并发安全问题的时候不要受到时间顺序的干扰,一切必须以先行发生原则为准。</p> <h2>volatile是原子性的吗?</h2> <p>volatile保证了可见性和有序性(禁止指令重排),但是不能保证原子性。</p> <p>假设线程A,读取了inc的值为10,然被阻塞,因未对变量进行修改,未触发volatile规则。线程B此时也读取inc的值,主存里inc的值依旧为10,做自增,然后立刻写回主存,值为11。此时线程A执行,由于工作内存里保存的是10,所以继续做自增,再写回主存,11又被写了一遍。所以虽然两个线程执行了两次increase(),结果却只加了一次。</p> <p>有人说,volatile不是会使缓存行无效的吗?但是这里线程A读取之后并没有修改inc值,线程B读取时依旧是10。又有人说,线程B将11写回主存,不会把线程A的缓存行设为无效吗?只有在做读取操作时,发现自己缓存行无效,才会去读主存的值,而线程A的读取操作在线程B写入之前已经做过了,所以这里线程A只能继续做自增了。</p> <h2>volatile和synchronized的区别</h2> <ul> <li>volatile本质是在告诉JVM当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。</li> <li>volatile仅能使用在变量级别;synchronized则可以使用在变量、方法和类级别的;</li> <li>volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性;</li> <li>volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。</li> <li>volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。</li> </ul> <h2>总结</h2> <pre><code>每个线程有自己的工作内存,工作内存中的数据并不会实时刷新回主内存,因此在并发情况下,有可能线程A已经修改了成员变量k的值,但是线程B并不能读取到线程A修改后的值,这是因为线程A的工作内存还没有被刷新回主内存,导致线程B无法读取到最新的值。 在工作内存中,每次使用volatile修饰的变量前都必须先从主内存刷新最新的值,这保证了当前线程能看见其他线程对volatile修饰的变量所做的修改后的值。 在工作内存中,每次修改volatile修饰的变量后都必须立刻同步回主内存中,这保证了其他线程可以看到自己对volatile修饰的变量所做的修改。 volatile修饰的变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同。 volatile保证可见性,不保证原子性,部分保证有序性(仅保证被volatile修饰的变量)。 指令重排序的目的是为了提高性能,指令重排序仅保证在单线程下不会改变最终的执行结果,但无法保证在多线程下的执行结果。 为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止重排序。</code></pre>

页面列表

ITEM_HTML