我们在考虑线程安全的时候,会遇到这样一类问题.
首先观察以下代码
package thread;
public class Test17 {
static boolean flag = true;
static int count = 0;
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
for (int i=0;i<10000;i++){
if (i==9999) flag = false;
}
});
Thread thread2 = new Thread(() -> {
while (flag){
count++;
}
System.out.println("满10000啦,循环次数为:"+count);
});
thread1.start();
thread2.start();
}
}
这里我写了两个线程,线程1对flag这个静态成员变量进行修改,但不过要等到循环结束的时候再对flag进行修改.线程2则是根据flag的值定义循环条件,每一次循环就让count变量自增一次.这么看下来,意思就是,一个线程写,一个线程读.运行下来我们看结果
不难发现循环次数和我们预期的10000次并不一样,甚至说是大相径庭
那么这又是为甚麽呢?
因为这里线程在执行读写操作的时候,系统发生了优化
因为这里thread1在循环的前9999次并没有对flag进行修改,那么线程2的while在进行读flag变量操作的时候,发现这个flag老大半天没有改动,所以它就自动优化了,不读了,这个值就是之前它读到的值(要问为甚麽这样子就算优化,因为读内存的操作也是很消耗系统资源的).所以我们看到线程2循环的次数很少,是寄存器偷懒少读了,进行了优化.
那么如果我们想避免这种情况的话,就要对那个读的变量加上volatile关键字,这样可以使该变量每次变化都被读取到.
循环次数直接彪到1万3,可以看出这个关键字的作用了
虽然这样会导致程序运行变得缓慢
但是我们宁愿算慢也不愿意算错,否则再快也是徒劳
特性
- 保证可见性,不保证原子性
- 当写一个volatile变量的时候,JVM会把该线程本地内存中的变量强制刷新到主内存中去,
- 这个操作会导致其他线程中的volatile变量缓存无效
- 禁止指令重排序
重排序是指编译器和处理器为了优化程序新跟那个而对指令序列进行排序的一种手段.重排序需要遵守一定规则:
- 重排序操作不会对存在数据依赖关系的操作进行重排序
比如:a=1;b=a;这样的指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序
- 重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变
比如:a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系, 所以可能会发生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。
重排序在单线程下一定能保证结果的正确性,但是在多线程环境下,可能发生重排序,影响结果,下例中的1和2由于不存在数据依赖关系,则有可能会被重排序,先执行status=true再执行a=2。而此时线程B会顺利到达4处,而线程A中a=2这个操作还未被执行,所以b=a+1的结果也有可能依然等于2。
使用volatile关键字修饰共享变量便可以禁止这种重排序。若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,volatile禁止指令重排序也有一些规则:
a.当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
b.在进行指令优化时,不能将对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
即执行到volatile变量时,其前面的所有语句都执行完,后面所有语句都未执行。且前面语句的结果对volatile变量及其后面语句可见。
volatile不合适的情况
不适合符合操作
package thread;
public class Test18 {
public volatile int a = 0;
public void increase() {
a++;
}
public static void main(String[] args) {
final Test18 test18 = new Test18();
for (int i=0;i<10;i++){
new Thread(){
public void run(){
for (int j=0;j<1000;j++){
test18.increase();
}
}
}.start();
}
while(Thread.activeCount()>1){
Thread.yield();
}
System.out.println(test18.a);
}
}
例如上述代码中,a++不是一个原子性操作,它由读取,加,赋值三步构成,所以结果并不能达到30000
解决方法:
- (1)采用synchronized
- 采用Lock
- 采用java并发包中的原子操作类,原子操作类是通过CAS循环的方式来保证其原子性的
原理
volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
(1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
(2)它会强制将对缓存的修改操作立即写入主存;
(3)如果是写操作,它会导致其他CPU中对应的缓存行无效。