面试连环炮:
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有什么区别
- volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
- volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
- volatile仅能实现变量的修改可见性和有序性,不能保证原子性;而synchronized则可以保证变量的可见性、有序性和原子性
- volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
- volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化