Java内存模型之可见性问题



本博客系列是学习并发编程过程中的记录总结。由于文章比较多,写的时间也比较散,所以我整理了个目录贴(传送门),方便查阅。

并发编程系列博客传送门


前言

之前的文章中讲到,JMM是内存模型规范在Java语言中的体现。JMM保证了在多核CPU多线程编程环境下,对共享变量读写的原子性、可见性和有序性。

本文就具体来讲讲JMM是如何保证共享变量访问的可见性的。

什么是可见性问题

我们从一段简单的代码来看看到底什么是可见性问题。


public class VolatileDemo {

    boolean started = false;

    public void startSystem(){
        System.out.println(Thread.currentThread().getName()+" begin to start system, time:"+System.currentTimeMillis());
        started = true;
        System.out.println(Thread.currentThread().getName()+" success to start system, time:"+System.currentTimeMillis());
    }

    public void checkStartes(){
        if (started){
            System.out.println("system is running, time:"+System.currentTimeMillis());
        }else {
            System.out.println("system is not running, time:"+System.currentTimeMillis());
        }
    }

    public static void main(String[] args) {
        VolatileDemo demo = new VolatileDemo();
        Thread startThread = new Thread(new Runnable() {
            @Override
            public void run() {
                demo.startSystem();
            }
        });
        startThread.setName("start-Thread");

        Thread checkThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    demo.checkStartes();
                }
            }
        });
        checkThread.setName("check-Thread");
        startThread.start();
        checkThread.start();
    }

}

上面的列子中,一个线程来改变started的状态,另外一个线程不停地来检测started的状态,如果是true就输出系统启动,如果是false就输出系统未启动。那么当start-Thread线程将状态改成true后,check-Thread线程在执行时是否能立即“看到”这个变化呢?答案是不一定能立即看到。这边我做了很多测试,大多数情况下是能“感知”到started这个变量的变化的。但是偶尔会存在感知不到的情况。请看下下面日志记录:


start-Thread begin to start system, time:1577079553515
start-Thread success to start system, time:1577079553516
system is not running, time:1577079553516   ==>此处start-Thread线程已经将状态设置成true,但是check-Thread线程还是没检测到
system is running, time:1577079553516
system is running, time:1577079553516
system is running, time:1577079553516
system is running, time:1577079553516
system is running, time:1577079553516
system is running, time:1577079553516
system is running, time:1577079553517
system is running, time:1577079553517
system is running, time:1577079553517
system is running, time:1577079553517
system is running, time:1577079553517
system is running, time:1577079553517
system is running, time:1577079553517
system is running, time:1577079553519
system is running, time:1577079553519
system is running, time:1577079553519
system is running, time:1577079553519
system is running, time:1577079553519
system is running, time:1577079553519
system is running, time:1577079553519
system is running, time:1577079553519
system is running, time:1577079553519

上面的现象可能会让人比较困惑,为什么有时候check-Thread线程能感知到状态的变化,有时候又感知不到变化呢?这个现象就是在多核CPU多线程编程环境下会出现的可见性问题。

Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程在工作内存中保存的值是主内存中值的副本,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。等到线程对变量操作完毕之后会将变量的最新值刷新回到主内存。

但是何时刷新这个最新值又是随机的。所以就有可能一个线程已经将一个共享变量更新了,但是还没刷新回主内存,那么这时其他对这个变量进行读写的线程就看不到这个最新值。这个就是多CPU多线程编程环境下的可见性问题。也是上面代码会出现问题的原因。

JMM对可见性问题的保证

在多CPU多线程编程环境下,对共享变量的读写会出现可见性问题。但是幸好JMM提供了相应的技术手段来帮我们规避这些问题,可以让程序正确运行。JMM针对可见性问题,主要提供了如下手段:

  • volatile关键字
  • synchronized关键字
  • Lock锁
  • CAS操作(原子操作类)

volatile关键字

使用volatile关键字修饰一个变量可以保证变量的可见性。所以对于上面的代码,我们只需要简单的修改下代码就可以让程序正确运行了。

private volatile boolean started = false;

使用volatile修饰一个共享变量可以达到如下的效果:

  • 一旦线程对这个共享变量的副本做了修改,会立马刷新最新值到主内存中去;
  • 一旦线程对这个共享变量的副本做了修改,其他线程中对这个共享变量拷贝的副本值会失效,其他线程如果需要对这个共享变量进行读写,必须重新从主内存中加载。

那么volatile具体是怎么达到上面两个效果的呢?其实volatile底层使用的是内存屏障来保证可见性的。

内存屏障(英语:Memory barrier),也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。大多数现代计算机为了提高性能而采取乱序执行,这使得内存屏障成为必须。

语义上,内存屏障之前的所有写操作都要写入内存;内存屏障之后的读操作都可以获得同步屏障之前的写操作的结果。因此,对于敏感的程序块,写操作之后、读操作之前可以插入内存屏障。
? ? ? ? ? ? ? ? ?

对内存屏障做下简单的总结:

  • 内存屏障是一个指令级别的同步点;
  • 内存屏障之前的写操作都必须立马刷新回主内存;
  • 内存屏障之后的读操作都必须从主内存中读取最新值;
  • 在有内存屏障的地方,会禁止指令重排序,即屏障下面的代码不能跟屏障上面的代码交换执行顺序,即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。

synchronized关键字

使用synchronized代码块或者synchronized方法也可以保证共享变量的可见性。只要如下修改上面的代码,我们就能得到正确的执行结果。


public synchronized void startSystem(){
    System.out.println(Thread.currentThread().getName()+" begin to start system, time:"+System.currentTimeMillis());
    value = 2;
    started = true;
    System.out.println(Thread.currentThread().getName()+" success to start system, time:"+System.currentTimeMillis());
}

public synchronized void checkStartes(){
    if (started){
        System.out.println("system is running, time:"+System.currentTimeMillis());
    }else {
        System.out.println("system is not running, time:"+System.currentTimeMillis());
    }
}

当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。我们发现锁具有和volatile一致的内存语义,所以使用synchronized也可以实现共享变量的可见性。

Lock接口

使用Lock相关的实现类也可以保证共享变量的可见性。其实现原理和synchronized的实现原理类似,这边也就不再赘述了。

CAS机制(Atomic类)

使用原子操作类也可以保证共享变量操作的可见性。所以我们只要如下修稿上面的代码就行了。

private AtomicBoolean started = new AtomicBoolean(false);

原子操作类底层使用的是CAS机制。Java中CAS机制每次都会从主内存中获取最新值进行compare,比较一致之后才会将新值set到主内存中去。而且这个整个操作是一个原子操作。所以CAS操作每次拿到的都是主内存中的最新值,每次set的值也会立即写到主内存中。

原文地址:https://www.cnblogs.com/54chensongxia/p/12084425.html

时间: 2024-07-31 08:59:01

Java内存模型之可见性问题的相关文章

Java-JUC(二):volatile对Java内存模型中的可见性、原子性、有序性影响

Java内存模型 Java内存模型-可见性 Java内存模型-原子性 Java内存模型-有序性 volatile-是否具有可见性? volatile-是否具有原子性? volatile-是否具有有序性? 原文地址:https://www.cnblogs.com/yy3b2007com/p/8901518.html

Java内存模型与共享变量可见性

此文已由作者赵计刚授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 注:本文主要参考自<深入理解Java虚拟机(第二版)>和<深入理解Java内存模型> 1.Java内存模型(JMM) Java内存模型的主要目标:定义在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节. 注意:上边的变量指的是共享变量(实例字段.静态字段.数组对象元素),不包括线程私有变量(局部变量.方法参数),因为私有变量不会存在竞争关系. 1.1.内存模型就是一张图: 说明: 所

并发编程-Java内存模型:解决可见性与有序性问题

背景 我们知道导致cpu缓存导致了可见性问题,编译器优化带来了有序性问题.那么如果我们禁用了cpu缓存与编译器优化,就能够解决问题,但是性能就无法提升了.所以一个合理的方案,就是按照一定规范来禁用缓存和编译器优化,即在某些情况下禁用缓存与编译器优化.Java内存模型就是这样的一个规范,用来解决可见性与有序性问题 概念 java内存模型本质上就是规范了JVM如何按照规则禁用缓存和编译器优化,既面向应用开发人员,也面向jvm的实现.这些规范包括了volatile.synchronized和final

【Java并发基础】Java内存模型解决有序性和可见性

前言 解决并发编程中的可见性和有序性问题最直接的方法就是禁用CPU缓存和编译器的优化.但是,禁用这两者又会影响程序性能.于是我们要做的是按需禁用CPU缓存和编译器的优化. 如何按需禁用CPU缓存和编译器的优化就需要提到Java内存模型.Java内存模型是一个复杂的规范.其中最为重要的便是Happens-Before规则.下面我们先介绍如何利用Happens-Before规则解决可见性和有序性问题,然后我们再扩展简单介绍下Java内存模型以及我们前篇文章提到的重排序概念. volatile 在前一

深入理解Java内存模型(四)——volatile

volatile的特性 当我们声明共享变量为volatile后,对这个变量的读/写将会很特别.理解volatile特性的一个好方法是:把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步.下面我们通过具体的示例来说明,请看下面的示例代码: class VolatileFeaturesExample { //使用volatile声明64位的long型变量 volatile long vl = 0L; public void set(long l) { vl = l;

深入理解Java内存模型(1 ) -- 基础(转载)

原文地址:http://www.infoq.com/cn/articles/java-memory-model-1 并发编程模型的分类 在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步(这里的线程是指并发执行的活动实体).通信是指线程之间以何种机制来交换信息.在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递. 在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信.在消息传递的并发模型里,线程之间没有公共状态,线

对java内存模型的认识

浅谈java内存模型        不同的平台,内存模型是不一样的,但是jvm的内存模型规范是统一的.其实java的多线程并发问题最终都会反映在java的内存模型上,所谓线程安全无非是要控制多个线程对某个资源的有序访问或修改.总结java的内存模型,要解决两个主要的问题:可见性和有序性.我们都知道计算机有高速缓存的存在,处理器并不是每次处理数据都是取内存的.JVM定义了自己的内存模型,屏蔽了底层平台内存管理细节,对于java开发人员,要清楚在jvm内存模型的基础上,如果解决多线程的可见性和有序性

(第三章)Java内存模型(中)

一.volatile的内存语义 1.1 volatile的特性 理解volatile特性的一个好办法是把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步.下面通过具体的示例来说明,示例代码如下: class VolatileFeaturesExample { volatile Long vl = 0L; //使用volatile声明64位的Long型变量 public void set(Long l) { vl = l; //单个volatile变量的写 } p

java内存模型-volatile

volatile 的特性 当我们声明共享变量为 volatile 后,对这个变量的读/写将会很特别.理解 volatile 特性的一个好方法是:把对 volatile 变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步.下面我们通过具体的示例来说明,请看下面的示例代码: class VolatileFeaturesExample { //使用volatile声明64位的long型变量 volatile long vl = 0L; public void set(long l) {