Java中的volatile关键字最全总结(二)
解决变量不可见的问题
既然出现了这一问题,那么该如何去解决它呢?
while (true) {
synchronized (myThread){
if(myThread.isTag()){
System.out.println("----------");
}
}
}
只需使用同步代码块将使用到共享变量的代码包裹起来即可,此时代码的执行流程如下:
1.main线程获取到锁
2.清空线程私有的内存空间
3.从主存中拷贝一份共享变量的副本到私有内存
4.对变量副本进行操作
5.将修改后的变量副本的值重新放回主存
6.main线程释放锁
由于main线程在使用tag时需要清空一次内存,并重新获取,这样就能够保证main线程在读取tag值的时候一定是最新的,而synchronized关键字的性能是比较差的,对于这种问题,使用 volatile 关键字将会显得更加优雅,我们只需要使用volatile关键字修饰共享变量即可:
private volatile boolean tag;
那么它的原理又是什么呢?首先子线程和main线程仍然会从主存中复制得到共享变量的副本,当子线程修改了共享变量但还未写入主存时,main线程获取到了共享变量的旧值,而由于共享变量被volatile修饰,所以当子线程将值写回主存时,会使其它线程的共享变量副本失效,失效后其它线程就会重新去主存获取一次值,这样也能够获取到最新的数据。
volatile能够保证不同线程对共享变量的操作可见性,当某个线程修改了共享变量的值时,其它线程便能够立即看到最新的值。
vloatile关键字还有一个特殊的性质,就是可以禁止指令的重排序,编译器为了提高程序的运行效率,它往往会对执行指令进行一个重排序,前提是不会影响到程序最终的运行结果,比如:
int a = 1;
int b = 2;
a = 3;
在这段程序中,按照从上到下的顺序,首先需要将a的值保存为1,再将b的值保存为2,最后重新将a的值保存为3,但为了提高效率,编译器可能会重新设置代码的执行顺序:
int a = 1;
a = 3;
int b = 2;
此时只需保存a的值为3,再保存b的值为2,这样就省略了一个步骤,提高了性能,需要注意的是指令重排序不能影响到程序最终的运行结果,所以语句 a = 3 肯定不会在 int a = 1 之前被执行。
来看一个例子:
public class VolatileDemo {
private static int a, b, i, j = 0;
public static void main(String[] args) throws InterruptedException {
while (true) {
a = 0;
b = 0;
i = 0;
j = 0;
Thread t1 = new Thread(() -> {
a = 1;
i = b;
});
Thread t2 = new Thread(() -> {
b = 1;
j = a;
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("i = " + i + ", j = " + j);
}
}
}
该程序中共有2个子线程,分别会去修改四个变量的值,然后输出被子线程修改后的变量值,这里调用join()方法是为了让主线程等待子线程执行完毕后才去输出变量值。我们可以猜测一下程序的运行结果,若是线程t1先执行,线程t2后执行,则i的值为0,j的值为1;若是线程t2先执行,线程t1后执行,则i的值为1,j的值为0;若是线程t1在执行过程中,t2也得到了执行,则i的值为1,j的值也为1,然而在运行程序之后,却得到了第4种结果:
......
i = 0, j = 1
i = 1, j = 1
i = 0, j = 1
i = 0, j = 0
i和j的值竟然均为0?这是为什么呢?原来,这是指令重排序导致的,编译器为了优化程序,很可能会将指令执行顺序重新排序,比如这样:
Thread t1 = new Thread(() -> {
i = b;
a = 1;
});
Thread t2 = new Thread(() -> {
j = a;
b = 1;
});
此时当线程t1在执行过程中,线程t2被执行,那么i和j的值就都为0了,这显然是违背我们正常思维的,为了防止这种情况的发生,可以使用 volatile 关键字修饰这些变量:
private volatile static int a, b, i, j = 0;
这样我们将无法再得到i和j均为0的情况了。
文章转自公众号:三友的java日记