Java中的volatile关键字最全总结(二)

pivoteic
发布于 2022-6-15 17:06
浏览
0收藏

 

解决变量不可见的问题

既然出现了这一问题,那么该如何去解决它呢?

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日记

标签
已于2022-6-15 17:06:07修改
收藏
回复
举报
回复
    相关推荐