Java内存模型可见性问题相关解析

 更新时间:2019年12月25日 15:10:04   作者:写代码的木公  
这篇文章主要介绍了Java内存模型可见性问题相关解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下

这篇文章主要介绍了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的值也会立即写到主内存中。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

相关文章

  • Kryo序列化及反序列化用法示例

    Kryo序列化及反序列化用法示例

    这篇文章主要介绍了Kryo序列化及反序列化用法示例,小编觉得挺不错的,这里分享给大家,需要的朋友可以参考下。
    2017-10-10
  • Java 常量池详解之class文件常量池 和class运行时常量池

    Java 常量池详解之class文件常量池 和class运行时常量池

    这篇文章主要介绍了Java 常量池详解之class文件常量池 和class运行时常量池,常量池主要存放两大类常量:字面量,符号引用,本文结合示例代码对java class常量池相关知识介绍的非常详细,需要的朋友可以参考下
    2022-12-12
  • java 值Document解析xml详细介绍

    java 值Document解析xml详细介绍

    这篇文章主要介绍了java 值Document解析xml详细介绍的相关资料,需要的朋友可以参考下
    2017-05-05
  • Java中Math类常用方法代码详解

    Java中Math类常用方法代码详解

    本文是小编最新给大家整理的关于Java中Math类常用方法的知识,通过实例代码给大家介绍的非常详细,感兴趣的朋友一起看看吧
    2017-07-07
  • mybatis plus动态数据源切换及查询过程浅析

    mybatis plus动态数据源切换及查询过程浅析

    这篇文章主要介绍了mybatis plus动态数据源切换及查询过程浅析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-12-12
  • IDEA配置tomcat服务器全过程

    IDEA配置tomcat服务器全过程

    这篇文章主要介绍了IDEA配置tomcat服务器全过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-11-11
  • 关于Java整合RocketMQ实现生产消费详解

    关于Java整合RocketMQ实现生产消费详解

    这篇文章主要介绍了关于Java整合RocketMQ实现生产消费详解,RocketMQ作为一款纯java、分布式、队列模型的开源消息中间件,支持事务消息、顺序消息、批量消息、定时消息、消息回溯等,需要的朋友可以参考下
    2023-05-05
  • java输入多个数据(不确定),排序,并求最大值的方法

    java输入多个数据(不确定),排序,并求最大值的方法

    今天小编就为大家分享一篇java输入多个数据(不确定),排序,并求最大值的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-07-07
  • spring单元测试下模拟rabbitmq的实现

    spring单元测试下模拟rabbitmq的实现

    这篇文章主要介绍了spring单元测试下模拟rabbitmq的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-05-05
  • jenkins安装及其配置笔记

    jenkins安装及其配置笔记

    这篇文章主要介绍了jenkins安装及其配置笔记,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2019-01-01

最新评论