【JAVA】 volatile关键字

JMM(java内存模型 Java Memory Model)
可见性
原子性
有序性

屏幕快照 2018-09-21 下午8.08.39.png

每个线程都有自己的一个工作内存,工作内存中存储着主内存变量的副本。当工作内存的变量发生改变后,会重新写回到主内存。

volatile满足可见性、有序性,但不保证原子性
volatile为什么不能保证原子性
假设主内存中a=0,两个线程都执行a++,第一个线程加载主内存的a到工作内存执行a++,准备写回主内存时线程挂起了,第二个线程加载主内存的a到工作内存执行了a++写回主内存中,此时a=1,凑巧第一个挂起的线程恢复了,也写回主内存,此时a又被写了一次1,正常的话主内存的a加了两次,应该是2

volatile不能保证原子性怎么解决

使用JUC的AtomicInteger

多线程指令重排案例一

多线程指令重排案例一.png

在线程1中的两句代码没有相互依赖关系,所以会可能会指令重排,单线程情况下没什么影响,但线程1指令运行先后次序会影响到线程2的结果。

多线程指令重排导致的问题二

多线程指令重排案例二.png

这里多线程执行method01和method02,method01里两句代码指令重排,method02里a输出可能是5也可能是6

内存间的相互操作

lock 将对象变成线程独占的状态
unlock 将线程独占状态的对象的锁释放出来
read 从主内存读数据
load 将从主内存读取的数据写入工作内存
use 工作内存使用对象
assign 对工作内存中的对象进行赋值
store 将工作内存中的对象传送到主内存当中
write 将对象写入主内存当中,并覆盖旧值

Volatile语义

Volatile的第一个语义就是保证此线程的可见性,一个线程对此变量的更改其他线程是立即可知的。也就是说 assign,store,write这三个操作是原子的,中间不会中断,会马上同步回主存,就好像直接操作主存一样,并通过缓存一致性通知其他缓存中的副本过期。普通变量可能会在assign,store,write之间插入其他操作,导致更改后的数据无法马上同步回主存,其他线程读取的可能是过期的旧数据。

Cpu与内存数据读取

在多核cup时代,不同线程可能在不同cup的核心中执行,由于cup处理速度和内存的读取速度大概相差大约一百倍,为了让cpu性能不浪费,cpu中做了一个高速缓存,cpu在处理的时候会把一批可能用到的数据载入到缓存中,等执行完毕再写回内存。
共享内存的变量与线程栈中的变量副本有可能在主存中,也有可能在cpu缓存中或者cpu寄存器中。

Q:为什么volatile不是线程安全的?
public class Counter {
 
    public volatile static int count = 0;
 
    public static void inc() {
        //这里延迟1毫秒,使得结果明显
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
        }
        count++;
    }
 
    public static void main(String[] args) {
        //同时启动1000个线程,去进行i++计算,看看实际结果
        for (int i = 0; i < 1000; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Counter.inc();
                }
            }).start();
        }
        //这里每次运行的值都有可能不同,可能为1000
        System.out.println("运行结果:Counter.count=" + Counter.count);
    }
}

运行结果还是没有我们期望的1000,下面我们分析一下原因

count++编译后最终并非一个原子操作,它由几个指令一起组合实现。
count++被分割成5个步骤(当然这个并不是确切的指令执行步骤),这5步不具有原子性,假如在完成过程中,其他线程就去读了主存的count变量,那明显导致了一个脏读现象。

屏幕快照 2019-01-28 下午1.55.42.png
屏幕快照 2018-09-21 下午8.42.12.png

read and load 从主存复制变量到当前工作内存
use and assign 执行代码,改变共享变量值
store and write 用工作内存数据刷新主存相关内容
其中use and assign 可以多次出现

这一些操作并不是原子性,也就是 在read load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,所以计算出来的结果会和预期不一样

对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工作内存的值是最新的

Example

假如线程1,线程2 在进行read,load 操作中,发现主内存中count的值都是5,那么都会加载这个最新的值

在线程1对count进行加1之后,会write到主内存中,主内存中的count变量就会变为6

线程2由于已经进行read,load操作,没有及时更新主内存数据,在进行加1运算之后,也会更新主内存count的变量值为6

导致两个线程及时用volatile关键字修改之后,还是会存在并发的情况。

导致这个问题的原因其实是因为volatile不具备锁操作,要解决此问题其实不难,就是将这五步变为原子操作,即保证线程一完成之前不能有其他线程读取count变量,对count变量加一个互斥锁即可达到,线程一在执行第①步前对count加锁,其他线程无法对count进行访问,线程一执行完第⑤步后释放锁,此刻开始才允许其他线程获取此变量。


变量的自增操作 i++,分三个步骤:
①从内存中读取出变量 i 的值
②将 i 的值加1
③将 加1 后的值写回内存
这说明 i++ 并不是一个原子操作。因为,它分成了三步,有可能当某个线程执行到了第②时被中断了,那么就意味着只执行了其中的两个步骤,没有全部执行,写回去也可能把别的线程写入主内存的数据覆盖掉。

举个例子,volatile在写单例时的作用

memory = allocate(); //1:分配内存空间
ctorInstance();   //2:初始化对象
singleton = memory; //3:设置singleton指向刚排序的内存空间

当线程A在执行上面伪代码时,2和3可能会发生重排序,因为重排序并不影响运行结果,还可以提升性能,所以JVM是允许的。如果此时伪代码发生重排序,步骤变为1->3->2,线程A执行到第3步时,线程B调用getsingleton方法,在判断singleton==null时不为null,则返回singleton。但此时singleton并还没初始化完毕,线程B访问的将是个还没初始化完毕的对象。当声明对象的引用为volatile后,伪代码的2、3的重排序在多线程中将被禁止!

参考链接:
https://juejin.im/post/6861885337568804871#heading-23

最后编辑于 :
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 缓存一致性问题 内存模型描述的是程序中各变量(实例域、静态域和数组元素)之间的关系,以及计算机系统将变量存...
    tracy_668阅读 1,181评论 0赞 3
  • 浅分析Java volatile关键字 大家好,前不久看了掘金一篇帖子原贴请点链接,那么今天就来给大家分享一下从这...
    ChaLLengerZeng阅读 620评论 0赞 3
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 43,059评论 11赞 349
  • ——人类社会由旧生物体制向新生物体制转型过程 人类社会的工业化文明,从一万多年前进入农业社会旧生物体制走出来仅仅用...
    诗哲魂阅读 268评论 0赞 0
  • 《》王红娟 小雨轻轻的下, 一路歌来,犹如甜蜜的蜜糖, 洪昌先生,行走校园, 脚步轻盈,满脸笑容, 小雨沙沙的下,...
    宏昌居士123阅读 172评论 0赞 1

友情链接更多精彩内容