今天忙里偷闲看到了volatile关键字的文章,正好发现好像现在面试和一些所谓的年薪百万机构都会说这个(手动狗头),所以就找了两篇文章看了看,下面是我的一些理解和案例。
好像很多文章都会拿synchronized关键字来比较对比,我专门去看了下两个关键字的差别和使用场景:
1、volatile是变量修饰符,而synchronized则作用于一段代码或方法。
2、volatile只是在线程内存和“主”内存间同步某个变量的值;而synchronized通过锁定和解锁某个监视器同步所有变量的值, 显然synchronized要比volatile消耗更多资源。
3、volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
4、volatile保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存中和公共内存中的数据做同步。
5、volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
线程安全包含原子性和可见性两个方面,Java的同步机制都是围绕这两个方面来确保线程安全的。
关键字volatile主要使用的场合是在多个线程中可以感知实例变量被修改,并且可以获得最新的值使用,也就是多线程读取共享变量时可以获得最新值使用。
关键字volatile提示线程每次从共享内存中读取变量,而不是私有内存中读取,这样就保证了同步数据的可见性。如上第二个栗子,可以得出volatile本身并不处理数据的原子性,而是强制对数据的读写及时的影响到主内存中;
区别大概就是这样的,然后分享一下新学到的一个知识点,
Java内存模型:
在Java里内存模型一共分为三种:
1:主内存:用来存储所有的变量;
2:工作内存:是每条线程自己的一个工作内存,虽然是自己的,但是也保存了主内存中线程使用到的变量副本;
3:线程:因为线程不可能自己直接去主内存拿变量,那样就太要不得了,所有就有个工作内存当中间人,所有的操作都是在工作内存中完成的。
关系图如下:
内存间的交互操作有很多,和volatile有关的操作为:
read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
对被volatile修饰的变量进行操作时,需要满足以下规则:
规则1:线程对变量执行的前一个动作是load时才能执行use,反之只有后一个动作是use时才能执行load。线程对变量的read,load,use动作关联,必须连续一起出现。-----这保证了线程每次使用变量时都需要从主存拿到最新的值,保证了其他线程修改的变量本线程能看到。
规则2:线程对变量执行的前一个动作是assign时才能执行store,反之只有后一个动作是store时才能执行assign。线程对变量的assign,store,write动作关联,必须连续一起出现。-----这保证了线程每次修改变量后都会立即同步回主内存,保证了本线程修改的变量其他线程能看到。
规则3:有线程T,变量V、变量W。假设动作A是T对V的use或assign动作,P是根据规则2、3与A关联的read或write动作;动作B是T对W的use或assign动作,Q是根据规则2、3与B关联的read或write动作。如果A先与B,那么P先与Q。------这保证了volatile修饰的变量不会被指令重排序优化,代码的执行顺序与程序的顺序相同。
一开始我以为volatile关键字和synchronized关键一样也是一个线程安全的,但是经过一个小栗子发现它不是个好人:
通过这个小demo发现了说volatile关键字是线程安全是不正确的,这是为什么?
volatile修饰的变量并不保值原子性,所以在上述的例子中,用volatile来保证线程安全不靠谱。
用Javap对这段代码进行反编译,为什么不靠谱简直一目了然:
getstatic指令把increase的值拿到了操作栈的顶部,此时由于volatile的规则,该值是正确的。
但是iconst_1和iadd指令在执行的时候increase的值很有可能已经被其他线程加大,此时栈顶的值过期。
putstatic指令接着把过期的值同步回主存,导致了最终结果较小。
volatile关键字只保证可见性,所以在以下情况中,需要使用锁来保证原子性:
-
运算结果依赖变量的当前值,并且有不止一个线程在修改变量的值。
-
变量需要与其他状态变量共同参与不变约束
并发三特征:可见性和有序性和原子性;
volatile通过新值立即同步到主内存和每次使用前从主内存刷新机制保证了可见性,
通过禁止指令重排序保证了有序性,
但是无法保证原子性;
好了,就写到这里,要进行巩固才行。