可见性:
引言:上一篇文章我们详细地讲了一下什么叫原子性,其中提到了用互斥变量来控制两个线程对缓冲区的访问,即wait()和signal()操作。
我们说在一个线程执行wait()操作进入临界区之后,对于另一个想访问的线程来说,由于mutex=0而进入不了临界区,这其实是不准确的。在其他线程看来,此时mutex值还可能是1。那么问题究竟出在哪呢,其实就是可见性问题。
有一个很重要的例子,摘自《Effective Java》第6条,来和大家一起进入可见性的学习。
public class StopThread {
private static boolean stopRequested;
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(new Runnable() {
public void run() {
// TODO Auto-generated method stub
int count=0;
while(!stopRequested) {
count++;
}
}
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
OK,代码很简单,我们分析一下。程序里面总共两个线程,一个主线程(main),一个子线程(backgroundThread)。StopThread有一个共享的布尔变量stopRequested。子线程通过判断该布尔变量来决定是否执行循环块代码,即count++操作。那么,在我们开始主线程之后,程序大概会经历多久停下来呢?(注意静态变量stopRequested默认是false)。你可能会说,由于主线程停止了1秒后才修改的布尔变量为true,所以,子线程大概会经过1秒执行完整个run()方法,也就是说整个程序大概会运行1秒钟。直接看结果吧:这个循环将一直执行下去!!!
这个问题的根源就在于多线程对共享变量的可见性!!
还是先来书上的定义:如果一个线程对于某个共享变量的进行更新之后,后续访问该变量的线程可以读取到该更改的结果,那么我们就说这个线程对于共享变量的的更新是可见的。
有点基础的朋友可能就恍然大悟,对于初次接触的同学还不好理解。问题就出在java的内存模型上!!!
先明白几点内存知识:
1.线程都有自己私有的工作区域(通常指寄存器),区别于主存或高速缓存。
2.这种设计的原因是,访问主存的速率是很慢的,线程要用共享变量时是将共享变量拷贝一份到自己的私有内存,之后再更新回去。
3.因此这就涉及到什么时候把值更新回去,以及其他线程看到的是不是更新后的值。
开篇的例子中出的问题在于:子线程并没有到内存里面去读新值,所以一直跳不出循环。这里还涉及jit编译器对循环体热点代码的优化,有兴趣的同学可以自己去查一下。
好了,问题已经给大家说清楚了,那么我们要怎么去解决这个问题呢?
其实很简单,就是让线程在读取共享变量时都到主存去取这个新值,在修改了这共享变量后都及时地写回主存就好了。
这里给大家介绍一个关键字volatile,我以一个看似过来人的语气告诉你,这是面试官钟爱的考点,嘿嘿>o<!
所以我们只需要用volatile修饰stopRequested就可以了。大家可以试一试哦,嗯,关于底层实现下一次再讲,回寝室!