>>分享Java编程技术,对《Java面向对象编程》等书籍提供技术支持 书籍支持  卫琴直播  品书摘要  在线测试  资源下载  联系我们
发表一个新主题 开启一个新投票 回复文章 您是本文章第 20556 个阅读者 刷新本主题
 * 贴子主题:  Java并发之volatile关键字内存可见性问题 回复文章 点赞(0)  收藏  
作者:flybird    发表时间:2020-03-23 19:34:20     消息  查看  搜索  好友  邮件  复制  引用

   Java并发之volatile关键字内存可见性问题

线程之间数据共享案例

我们先来看一个场景:

Main函数启动后,调用一个线程向list中添加数据。List的size为5的时候,设置变量flag为true.然后,主线程根据flag的值进行其他操作。

代码如下:

点击在新窗口中浏览原图
CTRL+鼠标滚轮放大或缩小

运行结果:

点击在新窗口中浏览原图
CTRL+鼠标滚轮放大或缩小

我们发现,当子线程输出flag为ture后,主线程也没有输出=====。

这是为什么呢?

线程在内存中运行简图

我们来看看上面程序在内存中怎么运行的

点击在新窗口中浏览原图
CTRL+鼠标滚轮放大或缩小

运行说明:

当程序运行的时候,先从main函数,主线程开始的,main线程先将flag=false 复制到自己程序的内存中;

这个时候开启了子线程,子线程同样将flag=false复制到自己程序内存中,在执行自己内部代码后,修改了flag的值,回写到主内存中(相对于程序自己的内存来说,内存中的数据是主内存。程序自己的内存其实就是复制了一份主内存的数据)。

如下图:

点击在新窗口中浏览原图
CTRL+鼠标滚轮放大或缩小

因为main线程没有刷新,没有从主线程获取最新的flag的值。所以,控制台上始终不能输出===。

结果分析

那么为什么会出现这种情况呢?【这里就需要知道两个概念:编译器和寄存器】

那是因为编译器会自动优化的结果。

编译器优化:在线程内,当读取到一个变量的时候,为了提高读写(存取)的速度,编译器在优化的时候,会先把变量读取到一个寄存器(对应上图子线程自己的内存或者是main线程自己的内存)中;以后在取这个变量的时候,就直接从寄存器中获取了;

当变量的值在本线程里面改变的时候,会同时把变量的新值同步到该寄存器中,以便保持一致;同时JVM就会向处理器发送一条指令,将这个变量所在的寄存器的值回写到系统内存(对应上图中的主内存)中。

造成数据不一致的原因:

当变量再因为别的其他线程操作而改变了值,该寄存器的值不会相应的改变,从而造成应用程序读取的值和实际的变量值不一致(如上图案例中,子线程修改了flag的值,但不会修改main线程寄存器里面的值。这个是站在变量角度来说的);

或者当该寄存器再因为别的其他线程改变了变量的值,原来变量的值不会改变,从而造成了应用程序读取的值和实际的变量值不一致(这个是从寄存器角度来说的。如上图案例中,main线程的寄存器里面是false,但是子线程已经修改成了ture).

从上面案例中,我们发现无论是主线程main函数还是子线程thread都是对变量flag进行操作的。这个时候,我们就说变量flag是线程之间共享数据了。而主内存(也就是系统内存非程序自己需要的内存)flag变量对所有共享这个变量的线程来说,都应该是可见的才可以。

那么这个时候,在Java中怎么实现线程之间共享数据的内存可见性呢?这里就是我们今天需要讲解的关键字:volatile。【ps:还有其他方案可以解决,如同步锁】

Volatile关键字

Volatile中文意思:易变的;不稳定的

Volatile关键字是一种类型修饰符,用它来声明的变量表示不可以别编译器未知因素更改。当编译器在编译过程中,遇到这个关键字声明的变量的时候,便一切都会对访问该变量的代码不再进行优化,从而可以提供特殊的地址来保证稳定访问。

通俗理解:当JVM遇到该关键字修饰的变量的时候,就会不允许编译器和处理器对指令序列进行重排(默认为了优化性能,JVM允许编译器和处理器对指令序列进行重排的)。

JVM对编译器指定的volatile规则表:

点击在新窗口中浏览原图
CTRL+鼠标滚轮放大或缩小

我们将flag用volatile修饰后,在运行程序,查看运行后结果:

点击在新窗口中浏览原图
CTRL+鼠标滚轮放大或缩小

我们可以看到,主线程输入了===,子线程也输出了当前标记为true。说明volatile起作用了。

Volatile和Synchronized 关键字的区别

1:Volatile是轻量级的同步策略;Synchronized是重量级的;

2:volatile不具备互斥性的,Synchronize是互斥的;

3:volatile不能够保证变量的原子性。

Volatile的使用场景

必备条件

在使用volatile的时候需要满足以下两个条件:

1:对变量的写操作不能依赖于当前的值。

比如上文中flag的值修改成true的时候,不受当前flag值的影响

2:该变量不能被包含在具有其他变量的不变式中。

比如int i ;y=i+1;这种情况不允许的。这个变量不能被其他变量作为不变式用。

只有在状态真正的独立于程序内其他内容的时候才可以使用volatile.

适用于场景一:状态标志

场景二:<em style="font-size:16px;">开销较低的读-写锁策略

场景三:单例中的双重校验

总结

Volatile可以解决多线程操作共享数据时候解决内存可见性问题。

简单理解:被volatile修饰的变量,编译器不会去优化调用该变量的程序(也就是不会把变量放到调用程序的寄存器中),程序调用的时候都是实时从主内存中获取到最新的数据。

凯哥个人博客:www.kaigejava.com

点击在新窗口中浏览原图
CTRL+鼠标滚轮放大或缩小



            

----------------------------
原文链接:https://blog.51cto.com/kaigejava/2477533
作者:凯哥

程序猿的技术大观园:www.javathinker.net



[这个贴子最后由 flybird 在 2020-03-23 19:34:20 重新编辑]
  Java面向对象编程-->Java注解
  JavaWeb开发-->在Web应用中访问Web服务
  JSP与Hibernate开发-->持久化层的映射类型
  Java网络编程-->基于UDP的数据报和套接字
  精通Spring-->绑定表单
  Vue3开发-->组合(Composition)API
  Java函数式接口和Stream流
  Java内存设置详解(含内存溢出问题的解决)
  Java并发编程的总结与思考
  Java 语言中十大“坑爹”功能!
  Java注解的定义和使用
  邀请您一起来祝福和祈祷,祈愿疫情早日消除,平安吉祥
  Java多线程volatile详解
  Eclipse使用指南:常用视图(View) 的用法
  Java入门实用代码:获取当前线程名称
  Java入门实用代码:集合转数组
  Java入门实用代码:链表元素查找
  Java入门实用代码:获取链表(LinkedList)的第一个和最后一...
  Java入门实用代码:打印矩形
  Java 入门实用代码:数组扩容
  Java 入门实用代码:取最大和最小值
  更多...
 IPIP: 已设置保密
楼主      
1页 0条记录 当前第1
发表一个新主题 开启一个新投票 回复文章


中文版权所有: JavaThinker技术网站 Copyright 2016-2026 沪ICP备16029593号-2
荟萃Java程序员智慧的结晶,分享交流Java前沿技术。  联系我们
如有技术文章涉及侵权,请与本站管理员联系。