变量不可见的两个原因
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实现可见性的原理
解决第一个导致不可见的因素(更新不及时):内存屏障,对于volatile修饰的变量,读操作时使用load指令重新从主存加载最新值进来;写操作时使用store指令将最新的值立刻写入主存。
解决第二个因素(指令重排): 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....