变量不可见的两个原因
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....