文章目录
volatile关键字是干嘛的
JMM有三个特性:可见性、有序性、原子性。Java用很多关键字来保证它们,volatile就是其中之一。
volatile关键字用于将Java变量标记为“正在存储在主存储器中”。更准确地说,这意味着每次对易失性变量的读取都将从计算机的主内存中读取,而不是从CPU缓存中读取,并且对易失性变量的每次写入都将被写入主内存中,而不仅是CPU缓存。
可变可见性问题
volatile关键字可确保跨线程更改变量的可见性。这听起来有点抽象,所以让我详细说明。
在多线程应用程序中,线程对非易失性变量进行操作,出于性能方面的考虑,每个线程在进行操作时都可以将主存储器中的变量复制到CPU缓存中。如果您的计算机包含多个CPU,则每个线程可能在不同的CPU上运行。这意味着,每个线程可以将变量复制到不同CPU的CPU缓存中。这在这里说明:
对于非易失性变量,不能保证Java虚拟机(JVM)何时将数据从主存储器读取到CPU缓存中,或何时将数据从CPU缓存写入主存储器。这可能会导致一些问题,我将在以下各节中进行解释。
设想一种情况,其中两个或多个线程可以访问一个共享对象,该共享对象包含一个声明为以下内容的counter变量:
public class SharedObject {
public int counter = 0;
}
假设只有线程1会递增counter变量,但是线程1和线程2都可能会counter不时读取变量。
如果counter变量没有声明volatile,就无法保证什么时候变量counter会被从CPU缓存写到主存储器。也就是说,CPU缓存中的counter可能与主存储器中的counter不同。
由于该变量尚未被另一个线程写回到主存储器,导致线程无法看到变量的最新值而导致的问题,被称为“可见性”问题。一个线程的更新对其他线程不可见。
volatile解决可见性
Java volatile关键字旨在解决变量可见性问题。通过对counter变量声明volatile,所有对该变量的写入counter 将立即写回到主存储器。同样,所有对counter变量的读取将直接从主存储器读取。
public class SharedObject {
public volatile int counter = 0;
}
因此以volatile 声明变量可确保其他线程对该变量的写入可见。
在上面给出的场景中,一个线程(T1)修改了counter ,而另一个线程(T2)读取了counter(但从未修改过),声明counter变量volatile足以保证T2可见性counter。
但是,如果T1和T2都在递增counter变量,则只对counter变量声明volatile就不够。以后再说。
volatile解决有序性
出于性能原因,允许Java VM和CPU对程序中的指令进行重新排序,只要指令的语义含义保持相同即可。例如,请查看以下说明:
整数= 1;
int b = 2;
a ++;
b ++;
这些指令可以重新排序为以下顺序,而不会丢失程序的语义:
int a = 1;
a ++;
int b = 2;
b ++;
然而,当变量之一是volatile变量时,指令重新排序提出了挑战。举例:
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;
}
}
一旦该update()方法将值写入days,新写入的值也将 被写入years和months还写入主存储器。但是,如果Java VM重新对指令进行排序,如下所示:
public void update(int years, int months, int days){
this.days = days;
this.months = months;
this.years = years;
}
在修改days变量时,months和years的值仍然写入主内存,但这一次是在将新值写入月和年之前(也就是说在day赋值后,month和year赋值前,将month和year的旧址写入乐主存)。因此,month和year的新值对于其他线程来说是不可见的。重新排序的指令的语义发生了变化。
详细可见:http://tutorials.jenkov.com/java-concurrency/volatile.html
推荐阅读:Java多线程(一)之volatile深入分析
volatile原理
为了提高处理器的执行速度,JMM在处理器和内存之间增加了多级缓存来提升。但是由于引入了多级缓存,就存在缓存数据不一致问题。
但是,对于volatile变量,当对volatile变量进行写操作的时候,JVM会向处理器发送一条lock前缀的指令,将这个缓存中的变量回写到系统主存中。
但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议
缓存一致性协议:每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。
所以,如果一个变量被volatile所修饰的话,在每次数据变化之后,其值都会被强制刷入主存。而其他处理器的缓存由于遵守了缓存一致性协议,也会把这个变量的值从主存加载到自己的缓存中。这就保证了一个volatile在并发编程中,其值在多个缓存中是可见的。
synchronized 关键字和 volatile 关键字的区别
- volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗,而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。
- 多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞
- volatile关键字能保证数据的可见性,但不能保证数据的原子性(就比如x++,看上去类似一个单独操作,实际上它是一个由读取-修改-写入操作序列组成的组合操作,必须以原子方式执行),volatile不是线程安全的。synchronized关键字两者都能保证。
- volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。
单例模式
public class MySingleton {
//使用volatile关键字保其可见性
volatile private static MySingleton instance = null;
private MySingleton(){}
public static MySingleton getInstance() {
try {
if(instance != null){//懒汉式
}else{
//创建实例之前可能会有一些准备性的耗时工作
Thread.sleep(300);
synchronized (MySingleton.class) {
if(instance == null){//二次检查
instance = new MySingleton();
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return instance;
}
}
需要注意 instance 采用 volatile 关键字修饰也是很有必要。
instance = new Singleton(); 这段代码其实是分为三步执行:
- 为 uniqueInstance 分配内存空间
- 初始化 uniqueInstance
- 将 uniqueInstance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。
使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。