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

简介: 我们在开发过程中,是不是频繁的写一些System.out.println()来验证程序的执行?切记在正式环境将打印语句去除!

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

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会尽可能的在空闲的时候去同步主存的共享变量。

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

目录
相关文章
|
29天前
|
存储 缓存 Java
优化Java代码中的内存使用:使用WeakHashMap解决内存泄漏问题
在Java应用程序中,内存泄漏是一个常见的问题,尤其是在处理大量对象时。本文将介绍如何使用WeakHashMap数据结构来解决内存泄漏问题,通过示例代码演示其在实际项目中的应用,从而提高Java代码的性能和可靠性。
|
4月前
|
存储 数据可视化 C++
提高代码效率的6个Python内存优化技巧
当项目变得越来越大时,有效地管理计算资源是一个不可避免的需求。Python与C或c++等低级语言相比,似乎不够节省内存。 但是其实有许多方法可以显著优化Python程序的内存使用,这些方法可能在实际应用中并没有人注意,所以本文将重点介绍Python的内置机制,掌握它们将大大提高Python编程技能。
98 0
|
2月前
|
IDE Linux 开发工具
内存泄漏检测工具Valgrind:C++代码问题检测的利器(一)
内存泄漏检测工具Valgrind:C++代码问题检测的利器
135 0
|
14天前
|
存储 开发者 Python
优化Python代码中的内存占用:实用技巧与最佳实践
本文将介绍如何优化Python代码中的内存占用,通过实用技巧和最佳实践,有效减少内存消耗,提升代码性能和可扩展性。
|
1月前
|
数据处理 开发者 Python
python 代码内存统计
【4月更文挑战第9天】
|
2月前
|
缓存 测试技术 开发工具
内存泄漏检测工具Valgrind:C++代码问题检测的利器(二)
内存泄漏检测工具Valgrind:C++代码问题检测的利器
38 0
|
4月前
小技巧分享:如何使用动态断点快速找到成对的 ABAP 内存 IMPORT 和 EXPORT 的代码位置
小技巧分享:如何使用动态断点快速找到成对的 ABAP 内存 IMPORT 和 EXPORT 的代码位置
24 0
|
4月前
|
Java 数据库连接
警惕内存泄漏与溢出!你的代码是否隐藏着致命危机?
警惕内存泄漏与溢出!你的代码是否隐藏着致命危机?
|
10月前
|
程序员 编译器 C语言
C语言指针理解 --- 代码配合图形讲解内存四区
C语言指针理解 --- 代码配合图形讲解内存四区
51 0
|
5月前
|
C++
c cpp 代码 数据竞争 分析, 以及 内存泄露 分析 工具 使用 demo
c cpp 代码 数据竞争 分析, 以及 内存泄露 分析 工具 使用 demo
30 0