Java之volatile关键字 volatile 关键字
Java的volatile关键字用于标记一个java变量“存储在主内存中”。更准确的说,被volatile标记的变量每次读取操作都将从计算机主内存(computer's main memory)读取,而不是从CPU缓存中读取,而且对volatile变量的每次写入都是写入到主内存,而不仅仅是写到CPU缓存。
实际上,从jdk5之后volatile关键字不仅仅保证了从内存中读写volatile变量。我会在下面对此进行介绍。
一:变量可见性问题
volatile关键字保证了同一变量跨线程更改的可见性问题。
在多线程应用程序中,线程对非volatile变量进行操作,出于性能考虑,每个线程在处理变量时,可以将他们从主内存中复制到CPU缓存中,如果你的计算机包含一个以上的CPU,每个线程可以在不同的CPU上运行。这意味着,每个线程可以将变量复制到不同CPU的CPU缓存上。如下图所示:
对于非volatile变量而言,不能保证Java虚拟机(JVM)何时将主内存中数据读取到CPU缓存中,或何时将CPU缓存中数据写入到主内存中。这就导致了几个问题。
假设一个场景,超过2个及其以上的线程访问一个声明包含计数器变量的共享对象:
public class ShareCounterObject{ public int counter = 0; }
假设仅线程1能修改counter变量并且每次递增+1,线程1和线程2都时时的访问counter变量。
如果counter变量不用volatile关键字声明,那么将不能保证counter变量每次更新的值从CPU缓存中写回主内存中。这意味着counter变量在CPU缓存中的值和主内存中不同。
如图所示:
因为一个线程更新没有立即回写到主内存,导致其他线程不能获取非volatile变量最新的值,这种被叫做“可见性”问题。一个线程更新了非volatile声明的共享变量对其他线程不可见
二:Java volatile保证可见性
volatile关键字目的就是解决可见性问题。声明counter是volatile变量,所有对counter的更改都会立即从CPU缓存写回到主内存中.同时,其他读取这个变量的操作也将是从主内存中读取。
声明volatile变量代码块儿:
public class ShareCounterObject{ public volatile int counter = 0; }
这样的话,线程1对counter变量更改,线程2对这个变量读取(但是不修改这个变量),volatile修饰的变量counter完全能保证在线程2写counter变量时的可见性。
如果线程1和线程2都递增counter变量,那么仅仅将其声明volatile是不够的。
三:Full volatile Visibility Guarantee
实际上volatile变量可见性保证volatile声明变量的本身,与他操作相关的变量也会保证。
规则1.如果线程A写入一个volatile变量,线程B随后读取同一个变量,那么线程A在写入volatile变量之前的所有可见的变量,线程B读取volatile变量后也可以看到。
规则2.如果线程A读取一个volatile变量,那么线程A读取volatile变量之前看到的所有变量都将从主内存中读取。
public class MyObject{ private int years; private int months; private volatile int days; public void update(int years ,int months ,int days){ this.years = years; this.months = months; this.days = days; } }
这个代码块中只有days是volatile变量。根据完整的可见性保证规则所示,一个线程执行update方法对这3个变量修改,最后修改volatile变量,因为在他前面的变量years,months对该线程是可见的,那么当days变量写回到主内存中时years,months变量也会被写入到主内存。
public class MyObject{ private int years; private int months; private volatile int days; public void update(int years ,int months ,int days){ this.years = years; this.months = months; this.days = days; } public int totalDays(){ int total = this.days; total += months * 30; total += years * 365; return total; } }
如上代码块儿所示,执行total方法时,首先读取days的值到total变量,在读取days变量值的时候,同时后面的months和years变量也是读入到主内存中。因此可以通过上面的读序列能够看到days,months,years的最新值。
四:指令重排序
在保证语义不变的情况下,JVM和CPU因为性能的原因可能会进行指令重排序。例如:
int a = 1; int b = 2; a++; b++;
如上代码,a,b变量在逻辑上毫无关系,指令重排序后可能执行顺序就是:
int a = 1; a++; int b = 2; b++;
指令重排序虽然很利于性能的优化,但是也会导致一些意外的问题,如上面的MyObject对象,如果对update方法进行重排序之后:
public void update(int years ,int months ,int days){ this.days = days; this.years = years; this.months = months; }
变成这样之后,修改days变量的时候months,years变量仍然写入主内存,但这次days新值写入主内存发生在months,years写入之前。因此months,years的新值对其他线程是不可见的。那么重新排序的语义就发生了变化。
Happens-Before规则就是Java推出的一个方案。后面我会针对这个在做一个详细介绍。
本文相关资料链接:http://tutorials.jenkov.com/java-concurrency/volatile.html
如有不对的地方请留言指正,互相学习!