volatile关键字的如何保证内存可见性,为啥不保证原子性

导读:本篇文章讲解 volatile关键字的如何保证内存可见性,为啥不保证原子性,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

volatile关键字的如何保证内存可见性,为啥不保证原子性

首先,我们来一段程序演示一下

/**
 * @program: mayun-quick_Netty
 * @description: volatile关键字的如何保证内存可见性
 * @author: Mr.Liu
 * @create: 2019-11-17 12:56
 **/
public class volatileTest {
    public static void main(String[] args) {
        ThreadDemo td = new ThreadDemo();
        new Thread(td).start();
        while(true){
            if (td.isTag()){
                System.err.println("================>>"+td.isTag());
                break;
            }
        }
    }

}
class ThreadDemo implements Runnable{
    private boolean tag = false;
    public boolean isTag(){
        return tag;
    }
    public void setTag(boolean tag){
        this.tag = tag;
    }
    @Override
    public void run() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        tag = true;
        System.out.println("tag = "+tag);
    }
}

有两个线程,一个修改tag的值为true,主线程获取tag的值做判断,结果:
volatile关键字的如何保证内存可见性,为啥不保证原子性
主线程未结束,当前tag的值还是false,所以没有打印========>>>true的值.
按理通过Runnable创建的线程访问的应该是共享数据,那为什么会出现这种情况?这就涉及到内存可见性。

JVM会为每个线程分配一个独立缓存提高效率
在这里插入图片描述
那么这个两个线程,一个是读(主线程),一个是写(线程1),我们让线程1睡了2s,说明,线程1先执行,每个线程都有一个独立的缓存,也就是说当线程1需要对主存的共享数值进行改变,它需要先把这个flag复制一份到缓存区中,
在这里插入图片描述
然后修改,将来再把这个值写回主存去,在写之前,主线程来了,它要读取现在在内存里面的值,现在是false,当然有一种情况,就是线程1在某个机会将flag=true写回去,
在这里插入图片描述
在这里插入图片描述
当时主线程用了while(true),这句话调用了系统底层代码,效率极高,高到主线程没有机会再次读取内存,这就是线程对共享数据操作的不可见。

内存可见性问题:当多个线程操作共享数据时,彼此不可见。
如何解决?同步锁。改写main方法

 ThreadDemo td = new ThreadDemo();
        new Thread(td).start();
        while(true){
            synchronized (td){
                if (td.isTag()){
                    System.err.println("================>>"+td.isTag());
                    break;
                }
            }
        }

在这里插入图片描述

但是用了锁,代表效率极低,但是我现在我不想加锁,但是有存在内存可见性的问题,我该怎么办?

关键字volatile:当多个线程进行操作共享操作时,可以保证内存中的数据可见。(内存栅栏,实时刷新)
被volatile关键字修饰的变量,在每个写操作之后,都会加入一条store内存屏障命令,此命令强制工作内存将此变量的最新值保存至主内存;在每个读操作之前,都会加入一条load内存屏障命令,此命令强制工作内存从主内存中加载此变量的最新值至工作内存。

我们可以认为它是直接在主存操作的,这个实时刷新的操作相比不加,性能略低,但是比加锁的效率显然高很多,低在哪?加了这关键字,JVM就不能进行指令重排序,无法优化代码执行

//修改代码中这个,添加了volatile关键字
private volatile boolean tag = false;

在这里插入图片描述
volatile相对synchronized是一种轻量级同步策略。但是注意:

  1. volatile不具备互斥性
  2. volatile不能保证变量的原子性

Java中long和double赋值不是原子操作,因为先写32位,再写后32位,分两步操作,这样就线程不安全了。如果改成下面的就线程安全了

private volatile long number = 8;

那么,为什么是这样?volatile关键字难道可以保证原子性?
volatile仅仅用来保证该变量对所有线程的可见性,但不保证原子性。但是我们这里的例子,volatile似乎是有时候可以代替简单的锁,似乎加了volatile关键字就省掉了锁。这不是互相矛盾吗

其实如果一个变量加了volatile关键字,就会告诉编译器和JVM的内存模型:这个变量是对所有线程共享的、可见的,每次jvm都会读取最新写入的值并使其最新值在所有CPU可见。所以说的是线程可见性,没有提原子性

下面我们用一个例子说明volatile没有原子性,不要将volatile用在getAndOperate场合(这种场合不原子,需要再加锁,如i++),仅仅set或者get的场景是适合volatile的。
例如你让一个volatile的integer自增(i++),其实要分成3步:

  1. 1)读取volatile变量值到local;
  2. 2)增加变量的值;
  3. 3)把local的值写回,让其它的线程可见。

这3步的jvm指令为:

mov    0xc(%r10),%r8d ; Load
inc    %r8d           ; Increment
mov    %r8d,0xc(%r10) ; Store
lock addl $0x0,(%rsp) ; StoreLoad Barrier

注意最后一步是内存屏障
什么是内存屏障(Memory Barrier)?
内存屏障(memory barrier)是一个CPU指令。基本上,它是这样一条指令:

  1. a) 确保一些特定操作执行的顺序;
  2. b) 影响一些数据的可见性(可能是某些指令执行后的结果)。

编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。

插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。

内存屏障另一个作用是强制更新一次不同CPU的缓存。

上面的 中间的几步(从Load到Store)是不安全的,中间如果其他的CPU修改了值将会丢失
所以volatile保证变量对线程的可见性,但不保证原子性

参考:https://www.cnblogs.com/figsprite/p/10779904.html

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/64713.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!