这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战
volatile关键字
前言
- java使用关键字volatile将java变量标记为“主内存存取”,可以理解为每次读取都从计算机主存而不是cpu缓存,每次写入都是写入主存而不仅仅是cpu缓存。
volatile特性
- 保证变量的可见性
- 禁指令重排
- 不保证原子性
变量可见性问题
- 在线程对没有volatile关键词修饰的变量进行操作的应用程序中,每个线程出于性能考虑都会将变量从主存复制到cpu缓存中。如果计算机包含多个cpu,那么线程运行在不同的cpu上,意味着每个变量都会被复制到不同的cpu缓存中。
- 对于没有volatile修饰的变量,无法保证jvm何时从主存中读取数据,也无法保证何时将数据刷新到主存中。这意味着当两个线程同时进行变量修改时,产生的实际结果不是我们所期待的结果,这也可以叫做变量的可见性问题。例如线程1和线程2同时将变量a = 0进行加1操作,由于不是最新主存中的值,可能两个线程执行后a的值依然为1.
java volatile 保证可见性
- java可见性的保证超出了volatile修饰的变量本身,可见性的保证如下:
- 如果线程a写入一个volatile变量,线程b读取同一个volatile变量,则线程a在写入volatile变量之前的所有变量,线程b在读取volatile变量后同样可见;
- 如果线程a读取了一个volatile变量,则所有在读取变量时线程a可见的变量,也会重新从主存中存取刷新。
public class myclass{
private int a;
private int b;
private volatile int c;
public void update(int a,int b,int c){
this.a = a;
this.b = b;
this.c = c;
}
}
- 当c变量被读取时,a 与 b 也会重新从主存中读取;当c变量被赋值时,a与b变量的值也会被刷新到主存中。
变量指令重排
- 处于性能考虑,jvm和cpu可以对程序中的指令进行重排,只要保持程序的语义不变。
int a = 1;
int b = 2;
a++;
b++;
- 可以重排为
int a = 1;
a++;
int b = 2;
b++;
- 因此现在回顾此前的代码,当指令发生重排序时,可能会存在新写入的值无法被写入到主存中的现象。在变量c被写入主存后,线程对变量a进行重新赋值,而volatile变量c已经将内容写入到主存中,因此导致变量a主存中的值与线程中副本的值不一致,其他线程调用就会存在数据问题。
public void update(int a,int b,int c){
this.b = b;
this.c = c;
this.a = a;
}
java volatile解决指令重排问题
- volatile提供了happens-before保证不会出现指令重排:
- 如果读、写其他变量发生在volatile之前,则读取和写入其他变量不能重排到volatile变量之后
- 如果读、写其他变量发生在volatile之后,则读取和写入其他变量不能重排到volatile变量之前
volatile的性能
- volatile的变量读取和写入都会进行主存访问,写入或读取主存耗费的性能代价大于cpu缓存。因此只有当需要的时候才采用volatile修饰变量是更好的选择。
volatile不足之处-不保证原子性
- 如果两个线程的操作均为基于获取volatile变量,同时获取后对其进行增加操作。由于该操作不是原子性的(例如i++),可能存在两个线程分别从主内存获取变量后,同时在自己的cpu缓存中进行操作。因此两个线程就产生了并发问题,这也是由于volatile不保证原子性导致的。
什么情况下可能存在问题
- 两个线程同时读取和写入变量,不是原子性操作时,这种情况推荐使用
synchronized或者java.util.concurrent。
后记
- 千古兴亡多少事?悠悠。不尽长江滚滚流。