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

简介: 我们在开发过程中,是不是频繁的写一些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会尽可能的在空闲的时候去同步主存的共享变量。

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

目录
相关文章
|
26天前
|
存储 JavaScript 前端开发
如何优化代码以避免闭包引起的内存泄露
本文介绍了闭包引起内存泄露的原因,并提供了几种优化代码的策略,帮助开发者有效避免内存泄露问题,提升应用性能。
|
6月前
|
SQL 关系型数据库 MySQL
实时计算 Flink版产品使用合集之idea本地测试代码,要增大 Flink CDC 在本地 IDEA 测试环境中的内存大小如何解决
实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStream API、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。
107 1
|
3月前
|
存储 缓存 JSON
一行代码,我优化掉了1G内存占用
这里一行代码,指的是:String.intern()的调用,为了调用这一行代码,也写了几十行额外的代码。
|
3月前
|
缓存 Java
Java内存管理秘籍:掌握强软弱幻四大引用,让代码效率翻倍!
【8月更文挑战第29天】在Java中,引用是连接对象与内存的桥梁,主要分为强引用、软引用、弱引用和幻象引用。强引用确保对象生命周期由引用控制,适用于普通对象;软引用在内存不足时可被回收,适合用于内存敏感的缓存;弱引用在无强引用时即可被回收,适用于弱关联如监听器列表;幻象引用需与引用队列配合使用,用于跟踪对象回收状态,适用于执行清理工作。合理使用不同类型的引用车可以提升程序性能和资源管理效率。
46 4
|
3月前
|
前端开发 JavaScript Java
揭开 JavaScript 垃圾回收的秘密——一场与内存泄漏的生死较量,让你的代码从此焕然一新!
【8月更文挑战第23天】本文通过多个实例深入探讨了JavaScript中的垃圾回收机制及其对应用性能的影响。首先介绍了基本的内存管理方式,随后分析了变量不再使用时的回收过程。接着,通过事件监听器未被移除及全局变量管理不当等场景展示了常见的内存泄漏问题。最后,文章介绍了使用`WeakRef`和`FinalizationRegistry`等现代API来有效避免内存泄漏的方法。理解并运用这些技术能显著提升Web应用的稳定性和效率。
91 0
|
4月前
|
安全 Java 开发者
探索Java内存模型:可见性、有序性和并发
在Java的并发编程领域中,内存模型扮演了至关重要的角色。本文旨在深入探讨Java内存模型的核心概念,包括可见性、有序性和它们对并发实践的影响。我们将通过具体示例和底层原理分析,揭示这些概念如何协同工作以确保跨线程操作的正确性,并指导开发者编写高效且线程安全的代码。
|
4月前
|
缓存 安全 Java
Java面试题:解释volatile关键字的作用,以及它如何保证内存的可见性
Java面试题:解释volatile关键字的作用,以及它如何保证内存的可见性
77 4
|
4月前
|
设计模式 安全 Java
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
78 1
|
4月前
|
安全 Java 开发者
Java面试题:Java内存模型解析,Java内存模型的基本概念和它的重要性,Java内存模型中的“可见性”和“有序性”,以及具体实现?
Java面试题:Java内存模型解析,Java内存模型的基本概念和它的重要性,Java内存模型中的“可见性”和“有序性”,以及具体实现?
59 1
|
5月前
|
缓存 Java 程序员
Java内存模型深度解析:可见性、有序性和原子性
在多线程编程中,正确理解Java内存模型对于编写高效且无bug的并行程序至关重要。本文将深入探讨JMM的三大核心特性:可见性、有序性和原子性,并结合实例分析如何利用这些特性来避免常见的并发问题。
60 1