目标:
1.了解什么是变量可见性问题
2.造成变量可见性问题原因
3.volatile关键字
前提说明:变量可见性问题发生在多线程,只有多线程才有变量可见性问题
问题1:如何在多线程之间共享变量?使用全局变量:静态变量或者共享对象
问题2:一个变量在线程1中被改变值了,在线程2中能看到此变量的最新值吗?不一定
有如下代码,执行结果是什么呢?线程会停止循环打印出i的值吗?
import java.util.concurrent.TimeUnit;
public class TestVolatile {
private static boolean isRunning = true;
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
int i=0;
while (isRunning){
i++;
}
System.out.println(i);
}
}).start();
try {
TimeUnit.SECONDS.sleep(2); //两秒之后
} catch (InterruptedException e) {
e.printStackTrace();
}
isRunning = false;
System.out.println("isRunning被设置为false了");
}
}
结果线程并没有停止,i的值并没有打印出来。这就是并发中的变量可见性问题。即所见非所得。
怎么才能可见呢?有两种方式
1.加synchronized
2.加volatile关键字修饰
import java.util.concurrent.TimeUnit;
public class TestVolatile {
// private static boolean isRunning = true;
//第二种方式
private static volatile boolean isRunning = true;
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
int i=0;
while (isRunning){
i++;
//第一种方式
// synchronized (this){
// i++;
// }
}
System.out.println(i);
}
}).start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
isRunning = false;
System.out.println("isRunning被设置为false了");
}
}
先了解下jmm(java内存模型)规范
1.共享变量必须放在主内存中
2.线程有自己的工作内存,线程只可操作自己的工作内存
3.线程要操作共享变量,需要从主内存读取到工作内存中,改变值后需要从工作内存同步到主内存中。
这就是线程安全问题根因,导致了变量可见性问题以及变量原子性问题,此处先讨论可见性问题。
问题1:变量在内存之间同步的时候被缓存了吗?
根据java内存模型我们可以知道变量修改完会同步到工作内存中。而且2秒肯定一定把变量同步到工作内存了。而内存之间的高速缓存并不是导致此问题的原因,这是更底层的开发者才会涉及的问题。java开发者可以不考虑各内存之间高速缓存问题。
问题2:使用共享变量前不会重新从主内存读取值吗?问题我们先放下。再了解一下指令重排
java编程语言的语义允许java编译器和微处理器进行执行优化,这些优化导致了与其交互的代码不再同步,从而导致看似矛盾的行为。
如图所示,这是一个简单的指令重排,导致结果跟代码执行结果不一致。
那么java中的指令重排什么时候会发生?。
这就不得不说到java的jit编译器(Just In Time Compiler)。java是解释性语言及编译性语言的混合体。
解释执行:即脚本,在执行时候,由语言的解释器将其一条条翻译成机器可识别的指令。
编译执行:将我们编写的程序直接编译成机器可以识别的指令码。即把整个批量编译。再分析一下代码执行。
java把java文件编译成类似字节码文件,并加载到jvm进程中执行
jit此时就把循环这块进行了isRunning缓存及指令重排。如上图所示。这就导致他并不会重新从主内存读取isRunning的值,并且主内存中此变量值改变了也不会导致线程中的while循环停止。所以此处就导致了可见性问题。
解决此问题可以使用volatile关键字,即上文中的代码中的第二种方式。第一种方式加synchronized解决原因先不讨论。
根据jmm中规定的happen before和同步规则,对某个volatile字段的写操作happends-before每个后续对该volatile字段的读操作。对volatile变量v的写入,与所有其它线程后续对v的读同步。所以volatile关键字满足一下功能:
1.禁止缓存;(volatile变量的访问控制符会加个ACC_VOLATILE,此处可以参考oracle官网相应描述)
2.对volatile变量相关的指令不做重排序。
到此时我们就基本明白了变量可见性问题的原因及解决方案。
之后会继续写一下java中的原子操作以及java变量的原子性问题。