如果我们只是需要并发的访问一两个值就使用同步,就显得开销有点大,在Java中还提供了volatile关键字来保证线程安全。
Volatile的中文意思是易变的,不稳定的,作为java中的关键词之一,用以声明变量的值可能随时会被别的线程修改。
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
使用volatile修饰的变量会强制将修改的值立即写入主存,主存中值的更新会使缓存中的值失效(非volatile变量不具备这样的特性,非volatile变量的值会被缓存,线程A更新了这个值,线程B读取这个变量的值时可能读到的并不是是线程A更新后的值)。
2)禁止进行指令重排序。
原子性、可见性、有序性
Volatile支持可见性和有序性,不支持原子性。
原子性:原子性通常指多个操作不存在只执行一部分的情况,如果全部执行完成那没毛病,如果只执行了一部分,那对不起,你得撤销(即事务中的回滚)已经执行的部分。
可见性:当多个线程访问同一个变量x时,线程1修改了变量x的值,线程1、线程2…线程n能够立即读取到线程1修改后的值。
有序性:即程序执行时按照代码书写的先后顺序执行。在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
volatile的原理和实现机制
观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
示例:
我们可以用前面那个银行转账的例子来感受一下volatile关键字的使用效果:
模拟账户–Account类:
public class Account {
private String name;// 名字
private double money;//余额
//构造方法
public Account(String name,double money) {
this.name = name;
this.money = money;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
}
Account相当于我们的临界资源,其中的private double money是需要在线程之间共享的,如果我们不加任何并发控制,也不使用volatile关键字声明,肯定是会出现数据安全的问题,这个我们之前已经验证过了。
下面我们将这个字段加上volatile关键字声明:
public class Account {
private String name;// 名字
private volatile double money;//余额
//构造方法
public Account(String name,double money) {
this.name = name;
this.money = money;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
}
运行结果:
经过多次的运行,以及对运行结果的分析,我们发现并没有出现数据安全性问题。
总结:
1、Volatile支持可见性和有序性,不支持原子性;sychronized支持可见性、有序性、原子性。
2、Volatile关键字声明变量后,不会进行指令重排序。
3、在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。