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的值也会立即写到主内存中。

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

相关文章

  • springboot实现全局异常处理及自定义异常类

    springboot实现全局异常处理及自定义异常类

    这篇文章主要介绍了springboot实现全局异常处理及自定义异常类,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-02-02
  • 为什么在foreach循环中JAVA集合不能添加或删除元素

    为什么在foreach循环中JAVA集合不能添加或删除元素

    今天给大家带来的文章是关于Java的相关知识,文章围绕着为什么在foreach循环中JAVA集合不能添加或删除元素展开,文中有非常详细的介绍及代码示例,需要的朋友可以参考下
    2021-06-06
  • RabbitMQ 如何解决消息幂等性的问题

    RabbitMQ 如何解决消息幂等性的问题

    这篇文章主要介绍了RabbitMQ 如何解决消息幂等性的问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-07-07
  • Mybatis配置之typeAlias标签的用法

    Mybatis配置之typeAlias标签的用法

    这篇文章主要介绍了Mybatis配置之typeAlias标签的用法,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-07-07
  • java ArrayList中的remove方法介绍

    java ArrayList中的remove方法介绍

    大家好,本篇文章主要讲的是java ArrayList中的remove方法介绍,感兴趣的同学赶快来看一看吧,对你有帮助的话记得收藏一下
    2022-01-01
  • 详解SpringMVC——接收请求参数和页面传参

    详解SpringMVC——接收请求参数和页面传参

    这篇文章主要介绍了详解SpringMVC——接收请求参数和页面传参,小编觉得挺不错的,现在分享给大家,也给大家做个参考。
    2016-12-12
  • Java Handler同步屏障浅析讲解

    Java Handler同步屏障浅析讲解

    同步屏障机制是什么?Handler发送的消息分为普通消息、屏障消息、异步消息,一旦Looper在处理消息时遇到屏障消息,那么就不再处理普通的消息,而仅仅处理异步的消息。不再使用屏障后,需要撤销屏障,不然就再也执行不到普通消息了
    2022-08-08
  • Java微信跳一跳操作指南

    Java微信跳一跳操作指南

    这篇文章主要为大家详细介绍了Java微信跳一跳操作指南,通过adb来控制手机进行操作,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-01-01
  • SpringBoot 快速实现 api 加密的方法

    SpringBoot 快速实现 api 加密的方法

    在项目中,为了保证数据的安全,我们常常会对传递的数据进行加密,常用的加密算法包括对称加密(AES)和非对称加密(RSA),本文给大家介绍SpringBoot 快速实现 api 加密,感兴趣的朋友一起看看吧
    2023-10-10
  • SpringBoot整合Elasticsearch游标查询的示例代码(scroll)

    SpringBoot整合Elasticsearch游标查询的示例代码(scroll)

    这篇文章主要介绍了SpringBoot整合Elasticsearch游标查询(scroll),本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-10-10

最新评论