volatile、Synchronized实现变量可见性的原理,volatile使用注意事项
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
volatile、Synchronized实现变量可见性的原理,volatile使⽤注
意事项
变量不可见的两个原因
Java每个线程⼯作都有⼀个⼯作空间,需要的变量都是从主存中加载进来的。
Java内存模型如下(JMM):
线程访问⼀个共享的变量时,都需要先从主存中加载⼀个副本到⾃⼰的⼯作内存中,经过⾃⼰修改后再更新到主存中去。
在这个过程中可能出现这种情况:线程A在⼯作内存中修改了变量1的值,但是还没有写⼊主存,这档⼝线程B将变量1加载到⾃⼰⼯作内存中。
显然,线程B拿到的不是变量1的最新值了。
变量可见性就是:这个变量被任何⼀个线程修改了,其他线程都能“看见”,也就是能取到变量最新的值。
重排序是指为了适合cpu指令执⾏机制,编译器、内存系统、处理器可能会对⼀些指令的执⾏顺序进⾏重排。
例如:
int a = 1; //line1
int b = 2; //line2
int s = a*b; //line3
line1 和 line2 可能会被重排颠倒位置,但是line3不会重排,因为Java单线程下会遵守 as-if-serial语义,简单的讲就是重排指令不会出现错误的结果。
在多线程下,指令重排则可能造成⼀些问题,例如:
class Example {
int a = 0;
boolean flag = false;
public void writer() {
a = 1;
flag = true;
}
public void reader() {
if (flag) {
int i = a +1;
}
}
}
线程A⾸先执⾏writer()⽅法,线程B线程接着执⾏reader()⽅法。
线程B在int i=a+1 时不⼀定能看到a已经被赋值为1,因为在writer()中,两句话顺序可能打乱:
线程A执⾏顺序: flag=true;(a=1;还没执⾏完或还没写到主存)
线程B执⾏:flag=true (⽽此时a=0) 产⽣了⼀些与我们预期之外的情况。
导致变量不可见的原因(1)更新不及时,(2)多线程交替执⾏时的指令重排序。
volatile实现可见性的原理
JVM线程⼯作时的原⼦性指令有:
read: 从主存读取⼀个变量的值的副本到线程的⼯作内存。
load:把read来的值赋给⼯作空间的变量中,然后就可以使⽤了。
use:要使⽤⼀个变量,先发出这个指令。
assign:赋值,给变量⼀个新值。
store:将⼯作空间的变量值运送到主存中。
write:将值写到主存的那个变量中。
上述操作必定是顺序执⾏的,但可不⼀定连续,中间可能插⼊其他指令。
为了保证可见性:关键就是保证load、use的执⾏顺序不被打乱(保证使⽤变量前⼀定刚刚进⾏了load操作,从主存拿最新值来),assign、wirte的执⾏顺序不被打乱(保证赋值后马上就是把值写到主存)。
所以使⽤内存屏障, CPU指令,可以禁⽌指令执⾏乱序:插⼊⼀个内存屏障,相当于告诉CPU和编译器指令顺序先于这个指令的必须先执⾏,后于这个命令的必须后执⾏。
解决第⼀个导致不可见的因素(更新不及时):内存屏障,对于volatile修饰的变量,读操作时在读指令use之前插⼊⼀条读屏障指令重新从主存加载最新值进来,保证了load、use指令的执⾏顺序不乱;写操作时在写指令assign之后插⼊⼀条写屏障指令,将⼯作内存变量的最新值⽴刻写⼊主存变量。
解决第⼆个因素(指令重排):由于读写数据时会在之前/后插⼊⼀条内存屏障指令,因此volatile可以禁⽌指令重排序。
Synchronized实现可见性原理
解决第⼀个因素:在加锁前会将⼯作内存的值全部重新加载⼀遍,保证最新;释放锁前将⼯作内存的值全部更新到主存;由于在带锁期间,没有其他线程能访问本线程正在使⽤的共享变量,这样就保证了可见性。
解决第⼆个因素:由于Synchronized修饰的代码块都是原⼦性执⾏的,即⼀旦开始做,就会⼀直执⾏完毕,期间有其他线程不可以访问本线程所使⽤的共享变量,这样,即便指令重排了也不会出现问题。
volatile不具有原⼦性可能导致的问题
经过前⾯的总结,可以看出Synchronized包裹的代码⾥的共享变量有可见性、指令不可排序性、原⼦性;volatile修饰的变量有可见性、指令不可排序性;volatile并不保证变量有原⼦性。
如果⽤volatile修饰变量希望保证它的原⼦性就可能出现问题,例如:
volatile int num = 0;
线程A:
num++;
线程B:
num++;
两个线程操作都进⾏num++的操作,理论上完事后 num 的值为2,但是num++ 的操作本⾝不是原⼦性的(包括读取 num原先的值、+1、把+1后的值写⼊num),volatile也不能使对num的操作变为原⼦性。
因此可能有num = 1的结果:
线程A:读取了num的值,为0,然后阻塞了。
线程B:读取num的值,还是为0,+1后⽴即写⼊主存,num = 1(体现可见性)。
线程A:恢复执⾏了,已经做完读取操作,⼯作内存中num = 0,继续执⾏。
num+1,写⼊内存,num = 1;
虽然这种情况发⽣的概率很⼩,但是并发量⼤时还是会出现不少这种情况的(可以⽤Synchronized、lock、AtomicInteger等解决)。
由于原⼦性的问题,volatile不适合修饰依赖⾃⾝的变量 : num++、a = a*2... 也不适合修饰不变式的变量(即volatile变量应该是独⽴的):a<b....。