volatile详细解读

面试连环炮:

volatile修饰的变量有什么特性?为什么不能保证原子性?volatile修饰的数组具有可见性吗?如何能够保证安全的操作数组?volatile与synchornized有什么异同?

volatile是java虚拟机提供的轻量级的同步机制,保证了可见性,有序性,不保证原子性

JMM(Java内存模型)是围绕着并发编程中原子性、可见性、有序性这三个特征来建立的,解释如下:

一、volatile修饰的变量有可见性,有序性,不保证原子性

原子性、有序性、可见性

1、原子性:

(1)原子的意思代表着——“不可分”;
(2)在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。原子性是拒绝多线程交叉操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。

2、可见性

线程执行结果在内存中对其它线程的可见性。

变量经过volatile修饰后,对此变量进行写操作时,汇编指令中会有一个LOCK前缀指令,加了这个指令后,会引发两件事情:

  • 发生修改后强制将当前处理器缓存行的数据写回到系统内存
  • 这个写回内存的操作会使得在其他处理器缓存了该内存地址无效,重新从内存中读取。

3、有序性

在本线程内观察,所有操作都是有序的(即指令重排不会导致单线程程序执行结果与排序前有任何差别)。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

二、为什么不能保证原子性

例如你让一个volatile的integer自增(i++),其实要分成3步:

1)读取volatile变量值到local; 

2)增加变量的值;

3)把local的值写回,让其它的线程可见。

这3步的jvm指令为:

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

 

StoreLoad Barrier就是内存屏障

内存屏障(memory barrier) 是一个CPU指令。基本上,它是这样一条指令: a) 确保一些特定操作执行的顺序; b) 影响一些数据的可见性(可能是某些指令执行后的结果)。编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。插入一个内存屏障, 相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。内存屏障另一个作用是强制更新一次不同CPU的缓存。例如,一个写屏障会 把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个cpu核心或者哪颗CPU执行的。

内存屏障和volatile什么关系?上面的虚拟机指令里面有提到,如果你的字段是volatile,Java内存模型将在写操作后插入一个写屏障 指令,在读操作前插入一个读屏障指令。这意味着如果你对一个volatile字段进行写操作,你必须知道:1、一旦你完成写入,任何访问这个字段的线程将 会得到最新的值。2、在你写入前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都刷新到缓存。 

明白了内存屏障这个CPU指令,回到前面的JVM指令:从Load到store到内存屏障,一共4步,其中最后一步jvm让这个最新的变量的值在所有线程可见,也就是最后一步让所有的CPU内核都获得了最新的值,但中间的几步(从Load到Store)是不安全的,中间如果其他的CPU修改了值将会丢失。

所以volatile不能保证i++操作的原子性

三、volatile的数组只针对数组的引用具有volatile的语义,而不是它的元素

一个线程向volatile的数组中设置值,而另一个线程向volatile的数组中读取。
比如seg.setValue(2),随后另一个线程调用seg.getValue(2),前一个线程设置的值对读取的线程是可见的吗?

我看书上说volatile的数组只针对数组的引用具有volatile的语义,而不是它的元素。

ConcurrentHashMap中也有这样的代码,我很疑惑,希望得到你的解答,谢谢。

    public class Seg {
        private volatile Object[] tabs = new Object[10];
        public void setValue(int index) {
            tabs[index] = new Object();
        }
        public Object getValue(int index) {
            return tabs[index];
        }
    }

我的回答

我做了实验证实这句话是正确的,“volatile的数组只针对数组的引用具有volatile的语义,而不是它的元素”。测试代码如下:

private static volatile Object[] tabs = new Object[10];

public static void main(String[] args) {
tabs[0]=1;

tabs=new Object[10];
}

参考:http://ifeve.com/volatile-array-visiblity/

四、使用AtomicReferenceArray原子性安全的操作数组

AtomicReferenceArray类提供了可以原子读取和写入的底层引用数组的操作,并且还包含高级原子操作。 AtomicReferenceArray支持对底层引用数组变量的原子操作。 它具有获取和设置方法,如在变量上的读取和写入。 也就是说,一个集合与同一变量上的任何后续获取相关联。
 

public class AtomicReferenceArrayDemo {
    public static void main(String[] args) {
        AtomicReferenceArray<Integer> array = new AtomicReferenceArray<>(5);
        //将1设置在0号索引位置
        array.set(0,1);
        System.out.println(array.get(0));

        //比较交换
        array.compareAndSet(0,1,2);
        System.out.println(array.get(0));
        
        //获取并设置值
        array.getAndSet(1,10);
        System.out.println(array.get(1));
        System.out.println(array.length());
    }
}

五、volatile和synchornized有什么区别

  1. volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  2. volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
  3. volatile仅能实现变量的修改可见性和有序性,不能保证原子性;而synchronized则可以保证变量的可见性、有序性和原子性
  4. volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
  5. volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
`const`和`volatile`是C语言的两个关键字,它们分别用于修饰变量和指针。下面详细介绍一下它们的作用和用法。 ## const `const`关键字用于限定一个变量的值不可被修改。它可以用于修饰普通变量、指针变量、函数参数和函数返回值等。 ### 修饰普通变量 当使用`const`修饰一个普通变量时,该变量的值就不能被修改。例如: ```c const int num = 10; num = 20; // 编译错误,无法修改const变量的值 ``` ### 修饰指针变量 当使用`const`修饰一个指针变量时,它就成为了一个指向常量的指针。也就是说,指针变量所指向的内存区域的值不能被修改。例如: ```c int num = 10; const int *p = &num; *p = 20; // 编译错误,无法通过指针修改const变量的值 ``` ### 修饰函数参数和返回值 使用`const`修饰函数参数有两个作用:一是告诉函数内部不要修改该参数的值;二是表示该函数不会修改该参数的值,可以提高程序的可读性和安全性。例如: ```c void print(const char* str) { printf("%s\n", str); } ``` 使用`const`修饰函数返回值可以避免返回值被修改。例如: ```c const char* get_string() { return "hello"; } ``` ## volatile `volatile`关键字用于告诉编译器,变量的值可能会被意外地修改,因此编译器不应该优化掉对该变量的操作。它可以用于修饰普通变量、指针变量和函数参数等。 ### 修饰普通变量 当使用`volatile`修饰一个普通变量时,它告诉编译器该变量的值可能会被意外地修改,因此编译器不应该对该变量的操作进行优化。例如: ```c volatile int num = 10; while (num == 10) { // do something } ``` ### 修饰指针变量 当使用`volatile`修饰一个指针变量时,它告诉编译器该指针所指向的内存区域的值可能会被意外地修改,因此编译器不应该对该指针的操作进行优化。例如: ```c volatile int *p = (int*) 0x00010000; *p = 10; ``` ### 修饰函数参数 使用`volatile`修饰函数参数可以告诉编译器该参数的值可能会被意外地修改,因此编译器不应该对该参数的操作进行优化。例如: ```c void do_something(volatile int* ptr) { *ptr = 10; } ``` 在嵌入式系统、操作系统内核等需要直接操作硬件的场景,使用`volatile`关键字可以确保变量的值与内存的实际值一致,从而避免出现意外的行为。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值