对内存可见性造成影响的代码

这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战

对内存可见性造成影响的代码

WangScaler: 一个用心创作的作者。

声明:才疏学浅,如有错误,恳请指正。

我们在开发过程中,是不是频繁的写一些System.out.println()来验证程序的执行?切记在正式环境将这些无用的打印语句删除。为什么?因为System.out.println()是一个同步操作,会影响性能。

内存可见性

可见性是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。

我们知道共享变量是存储在主内存中的,每个线程使用的时候从主内存复制到自己的工作内存,所以线程在操作这个变量的时候是在自己的工作内存中,操作完毕才会将值刷新到主内存,也就是说其他线程刷新了主内存的值,而我们当前线程是无法感知的,会继续操作自己工作空间的值,进而最终导致主内存的共享变量不是我们预期的结果

接下来以一个例子来证实内存的不可见。

package com.wangscaler.jmm;
​
import java.util.concurrent.TimeUnit;
​
/**
 * @author WangScaler
 * @date 2021/8/3 13:53
 */public class Visibility {
    boolean flag = true;
​
    public  void changeFalse() {
        this.flag = false;
    }
​
    public static void main(String[] args) throws InterruptedException {
        Visibility visibility = new Visibility();
​
        new Thread(() -> {
            System.out.println("3s后flag修改为flase");
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (Exception e) {
                e.printStackTrace();
            }
            visibility.changeFalse();
            System.out.println("ChangeFalse线程修改flag的值为: " + visibility.flag);
        }, "ChangeFalse").start();
        while (visibility.flag == true) {
        }
        System.out.println("主线程得到flag的值为false");
    }
}
复制代码

这个例子打印完

3s后flag修改为flase
ChangeFalse线程修改flag的值为: false
复制代码

会进入无限循环。我们看到ChangeFalse这个线程已经将flag修改为false,但是线程还没没终止,这就印证了我们的说法。

  • main线程,将将主存中flag的值复制到自己的工作内存中。
  • 接着启动线程ChangeFalse,此时该线程也会将主存中flag的值复制到该线程的工作内存中。

  • 接着while循环从自己工作内存中读取flag的值,一直为true,一直循环。

    image-20210804102734610.png

  • 3s后线程ChangeFalse将flag的值从自己的工作内存中修改为false。

  • 虽然线程ChangeFalse工作内存flag的值被修改了,但是什么时候刷新到主内存是不确定的

  • 即使立即刷新到主内存,但是其他线程也是无法感知的。

  • 所以while循环一直读取的自己工作内存的flag,就处于无限循环中

    image-20210804104249924.png

如何保证可见性

这样肯定达不到我们的预期,那么我们如何达到我们想要的目的呢?使用关键字volatile!

修改上述的代码为boolean flag = true;修改为volatile boolean flag = true;即可。

使用这个关键字之后,当ChangeFalse线程修改完flag,会立即刷新到主存,同时使其他线程的工作内存的值失效,从而从主内存中重新拉取新的flag值,所以当ChangeFalse线程修改完flag,main线程感知到立即从主存中拉取新值,从而终止循环。

案例

经典的案例就是单例模式,我们在设计模式五—-单例模式曾讲述过,这里不再赘述,感兴趣的朋友可以去看看。

System.out.println()对可见性的影响

package com.wangscaler.jmm;
​
import java.util.concurrent.TimeUnit;
​
/**
 * @author WangScaler
 * @date 2021/8/3 13:53
 */public class Visibility {
    boolean flag = true;
​
    public  void changeFalse() {
        this.flag = false;
    }
​
    public static void main(String[] args) throws InterruptedException {
        Visibility visibility = new Visibility();
​
        new Thread(() -> {
            System.out.println("3s后flag修改为flase");
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (Exception e) {
                e.printStackTrace();
            }
            visibility.changeFalse();
            System.out.println("ChangeFalse线程修改flag的值为: " + visibility.flag);
        }, "ChangeFalse").start();
        while (visibility.flag == true) {
            System.out.println("-----------------循环中------------------");
        }
        System.out.println("主线程得到flag的值为false");
    }
}
复制代码

仅仅是在while循环中,增加了一个System.out.println(),就实现了可见性,所以在生产环境尽量将无用的System.out.println()删除。

为什么我们随手写的打印语句,竟然对测试结果造成了不可预知的影响呢?怀着好奇心,我打开了源码。

public void println(String x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}
复制代码

没错我们熟悉的打印语句是个同步方法。而synchronized()不仅能保证可见性还能保证原子性,那么使用打印语句导致循环终止停止就 不言而喻了。那么System.out.println()造成的影响到底是不是synchronized()导致的呢?我们验证一下。

synchronized()对可见性的影响

package com.wangscaler.jmm;
​
import java.util.concurrent.TimeUnit;
​
/**
 * @author WangScaler
 * @date 2021/8/3 13:53
 */public class Visibility {
    boolean flag = true;
​
    public void changeFalse() {
        this.flag = false;
    }
​
    public static void main(String[] args) throws InterruptedException {
        Visibility visibility = new Visibility();
​
        new Thread(() -> {
            System.out.println("3s后flag修改为flase");
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (Exception e) {
                e.printStackTrace();
            }
            visibility.changeFalse();
            System.out.println("ChangeFalse线程修改flag的值为: " + visibility.flag);
        }, "ChangeFalse").start();
        while (visibility.flag == true) {
            synchronized (Visibility.class) {
​
            }
        }
        System.out.println("主线程得到flag的值为false");
    }
}
复制代码

如我们预期,程序正常终止,所以更加证实了System.out.println()导致可见性的根源在于synchronized。

TimeUnit.MILLISECONDS.sleep(5);对可见性的影响

package com.wangscaler.jmm;
​
import java.util.concurrent.TimeUnit;
​
/**
 * @author WangScaler
 * @date 2021/8/3 13:53
 */public class Visibility {
    boolean flag = true;
​
    public void changeFalse() {
        this.flag = false;
    }
​
    public static void main(String[] args) throws InterruptedException {
        Visibility visibility = new Visibility();
​
        new Thread(() -> {
            System.out.println("3s后flag修改为flase");
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (Exception e) {
                e.printStackTrace();
            }
            visibility.changeFalse();
            System.out.println("ChangeFalse线程修改flag的值为: " + visibility.flag);
        }, "ChangeFalse").start();
        while (visibility.flag == true) {
            TimeUnit.MILLISECONDS.sleep(5);
        }
        System.out.println("主线程得到flag的值为false");
    }
}
复制代码

经过测试不仅是同步的方法导致可见性,使用了TimeUnit.MILLISECONDS.sleep(5);也会达到相同的预期,你也许会说是不是源码中也使用了同步的方法,但是翻阅似乎并未发现,查阅资料一致的说法是线程在空闲的时候会去主存中刷新工作内存的值。

TimeUnit.MILLISECONDS.sleep(5);底层还是调用的Thread.sleep(ms, ns);,所以毫无疑问直接使用Thread.sleep(ms, ns);也会达到相同的效果。

public void sleep(long timeout) throws InterruptedException {
    if (timeout > 0) {
        long ms = toMillis(timeout);
        int ns = excessNanos(timeout, ms);
        Thread.sleep(ms, ns);
    }
}
复制代码

结论

1、当有synchronized()同步机制的时候,会保证可见性。

2、jvm会尽可能的在空闲的时候去同步主存的共享变量。

网上一些说法也认为其实第一种同步机制导致的可见性,其实是第二种造成的假象,原文链接。我们只需知道这两种情况都会造成内存的可见性,到底是什么原因导致的,有兴趣的朋友可以深入了解一下,如果你知道答案,希望评论告知我,感激不尽。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享