前言
- 在
Java
中,volatile
关键字十分重要 - 通过这篇文章,相信您对
volatile
关键字会更加理解
1. 定义
Java
中的一个关键字/修饰符
2. 作用
保证在多线程环境下,某个变量具有可见行、有序性,但是不保证代码的原子操作性
3. Java中的线程安全问题
Java
模型规定所有的变量都要存在主存(类似计算机中物理内存的概念)中,每个线程都有自己的工作内存(类似计算机中高速缓存的概念),线程对变量的读取和写入操作都需要先在工作内存中进行,不能直接对主存进行操作。
在执行程序时,会将运算的变量从主存中复制一份到工作内存中,这样线程就可以从工作内存中读取变量的值,当运算结束后再写入主存中,保证了指令执行的效率,但是会出现同一个变量在两个工作内存中的值不一致问题,比如以下代码:
a = a + 1;
观察以上代码,假如当前a
在主存中的值为0,如果有两个线程先后从主存中读取并且把a = 0
的值都存在了各自的工作内存中,然后线程1执行a = a + 1
的操作并且把a = 1
刷新到主存中;但是此时线程2工作内存中的a
值依然是0,执行了a = a + 1
操作之后a
的值依然是1,和我们预想的结果不一致,这就导致了线程安全问题。
4. 可见行
那么什么是可见行呢?在多线程情况下,某个共享变量如果被其中一个线程给修改了,那么其他的线程能够马上知道该共享变量已经是修改状态了,当其他线程要用这个变量时,它会从内存中取,而不是从自己的工作空间取。例如下面这句代码:
a = a + 1;
比如有两个线程都要执行以上的代码,当线程1对a进行了加1操作并把数据写入主存中,线程2就会立即知道自己的工作空间的a已经被修改过了,当它要执行加1的操作时,就会到内存中重新读取,这样就保证了数据的安全性。
被声明了
volatile
关键字的变量,具有可见行的性质
5. 有序性
实际上,当我们编写好代码后,虚拟机不一定会按照我们编写的顺序执行代码,比如以下两行代码:
int a = 1;
int b = 2;
虚拟机在进行代码编译优化的时候,对那些改变执行顺序之后不会影响最终结果的代码是会进行重排序的,
5.1 为什么要进行重排序
因为假如执行第一句代码需要100ms,而执行第二句代码需要10ms,而且对a,b的值并不会造成影响,那么虚拟机是有可能先选择后者执行的。
5.2 重排序造成的影响
对代码进行重排序之后,虽然对变量的值不会造成影响,但有可能会产生线程安全问题,比如以下代码:
public class NoVisibility{
private static boolean ready;
private static int number;
private static class Reader extends Thread{
public void run(){
while(!ready){
Thread.yield();
}
System.out.println(number);
}
}
public static void main(String[] args){
new Reader().start();
number = 42;
ready = true;
}
}
观察以下代码可以看出打印的结果是42,不过假如虚拟机对number=42
和ready=true
进行了重排序,那么打印结果是0,所以对代码进行重排序,是会导致线程安全问题的。
假如把上面的number
变量声明为volatile
,那么number=42
一定会比ready=true
先执行,这样就保证了代码的有序性。
被声明了
volatile
关键字的变量,会保证代码的有序性
6. 原子操作
那么声明了volatile关键字的一个变量真能保证在多线程的环境下安全使用呢,答案是否定的,因为在Java
里面的运算并非都是原子操作。
原子操作:即一个操作或者多个操作,要么全部执行并且中途不会被任何因素打断,要么都不执行。
比如说以下代码:
int a = b + 1;
处理器在处理以上代码时,会处理以下三个操作:
- 从内存中读取
b
的值 - 进行
a = b + 1
的运算 - 把
a
的值写入内存中
而这三个操作不一定会连续执行,处理器有可能执行完第一个操作之后,就跑去执行别的操作了。举个例子便能证明声明了volatile
关键字的变量无法保证线程安全:
public class Test{
public static volatile int t = 0;
public static void main(String[] args){
Thread[] threads = new Thread[10];
for(int i = 0; i < 10; i++){
threads[i] new Thread(new Runnable(){
@Override
public void run(){
for(int j = 0; j < 1000; j++){
t = t + 1;
}
}
});
threads[i].start();
}
while(Thread.activeCount() > 1){
Thread.yield();
}
System.out.println(t);
}
}
观察以上代码,你可能会认为打印的结果为10000,并非一定如此;假如线程1,2先后读取了t = 0
的值,随后线程1执行t = t + 1
的操作,此时t = 1
,但是虚拟机并没有把t = 1
的结果写入内存中,这个时候虚拟机却跑去执行线程2的操作,但是注意此时线程2的t
值还是0,当执行了t = t + 1
的操作后,t
的值还是1,那么就有问题了,执行了两次t = t + 1
操作后结果还是1,并没有按照我们预期的结果来进行运算,这就造成了线程安全问题了。
被声明了
volatile
关键字的变量并不具有原子操作性,依然存在线程安全问题。
7. 总结
- 被声明了
volatile
关键字的变量,具有可见行的性质 - 被声明了
volatile
关键字的变量,会保证代码的有序性 - 被声明了
volatile
关键字的变量并不具有原子操作性,依然存在线程安全问题
8. 参考文献
- 苦逼的码农:《彻底搞懂volatile关键字》
- 周志明:《深入理解Java虚拟机》
网友评论