变量可见性经常作为大厂的面试题,难倒很多精通多线程并发的面试者,这里列出这个题目尽可能多的答案。综合考虑了变量可见性,线程上下文切换,以及JIT优化。
如下一段程序,期望当主线程A设置stop变量为true的时候,线程B退出循环。 但实际上线程B看不到更新后的值,从而一直循环下去。
public class CPUCacheTest {
private static boolean stop = false;
public static void main(String[] args){
Thread a = new Thread("B"){
public void run(){
while (!stop) {
int a = 1;
}
System.out.println("exit");
}
};
a.start();
pause(100);
//停止标记,但未能停止线程B
stop = true;
}
public static void pause(int time){
try {
TimeUnit.MILLISECONDS.sleep(time);
}catch(Exception ex){
}
}
}
原因分析
在书中第三章已经说过,多线程下变量的可见性,需要添加volatile
关键字,保证stop变量能被其他线程看到
private static volatile boolean stop = false;
场景一
判断如下代码是否也能保证线程B退出
Thread a = new Thread("B"){
public void run(){
while (!stop) {
System.out.println("in loop");
}
System.out.println("exit");
}
};
答案,能退出,因为方法out.println()
实际上如下实现,synchronized保证变量可见性
private void write(String s) {
synchronized (this) {
.....
}
}
场景二
判断如下代码是否也能保证线程B退出,调用pause方法,休眠1毫秒
Thread a = new Thread("B"){
public void run(){
while (!stop) {
pause(1);
}
System.out.println("exit");
}
};
答案,能退出,pause方法引起线程休眠,再次唤醒的时候,上下文切换,线程B能得到stop最新的变量
场景三
判断如下代码是否也能保证线程B退出,定义int类型的变量b,在循环里自增
public class CPUCacheTest {
private static /* volatile */ boolean stop = false;
static int b = 1;
public static void main(String[] args){
Thread a = new Thread("B"){
public void run(){
while (!stop) {
b++
}
System.out.println("exit "+b);
}
};
System.out.println("start");
a.start();
pause(100);
stop = true;
}
答案,不能退出,没有任何原因触发变量可见性
场景四
判断如下代码是否也能保证线程B退出,定义Integer类型的变量b,在循环里自增
public class CPUCacheTest {
private static /* volatile */ boolean stop = false;
static Integer b = 1;
public static void main(String[] args){
Thread a = new Thread("B"){
public void run(){
while (!stop) {
b++
}
System.out.println("exit "+b);
}
};
System.out.println("start");
a.start();
pause(100);
stop = true;
}
答案,能退出,因为如上代码b++
实际的操作如下。
int temp = b.intValue();
temp++;
b = new Integer(temp);
因为Integer定义如下,因为变量是final,保证了变量可见性
private final int value;
public Integer(int value) {
this.value = value;
}
场景五
判断如下代码是否也能保证线程B退出,while循环里定义Integer 变量,并自增
public class CPUCacheTest {
private static /* volatile */ boolean stop = false;
public static void main(String[] args){
Thread a = new Thread("B"){
public void run(){
while (!stop) {
Integer c = 1;
c++;
}
System.out.println("exit "+b);
}
};
System.out.println("start");
a.start();
pause(100);
stop = true;
}
答案,不能退出,在while循环里,变量c的操作被JIT优化程如下代码
Thread a = new Thread("B"){
public void run(){
while (!stop) {
//代码被消除
}
System.out.println("exit "+b);
}
};
场景六
判断如下代码是否也能保证线程B退出,while循环里定义Integer 变量c,自增后赋值给b
public class CPUCacheTest {
private static /* volatile */ boolean stop = false;
static int b = 1;
public static void main(String[] args){
Thread a = new Thread("B"){
public void run(){
while (!stop) {
Integer c = 1;
c++;
b=c;
}
System.out.println("exit "+b);
}
};
System.out.println("start");
a.start();
pause(100);
stop = true;
}
答案,不能退出,变量c 赋值到b操作会提取到循环外执行,这涉及到JIT编译器优化,表达式提升(hoisting),运行代码如下
Thread a = new Thread("B"){
public void run(){
Integer c = 1;
c++;
b=c;
while (!stop) {
}
System.out.println("exit "+b);
}
};
场景七
判断如下代码是否也能保证线程B退出,while循环里使用StringBuffer
Thread a = new Thread("B"){
public void run(){
while (!stop) {
StringBuffer sb = new StringBuffer();
sb.append("abc");
}
System.out.println("exit "+b);
}
};
答案,能退出,StringBuffer 的append方法有关键字synchronzied
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
场景八
同场景3,但运行这个程序加上虚拟机参数 -Xint ,程序能正确退出么?
答案 在第8章JIT说过,-Xint 是让Java解释执行,这样,不会涉及到变量可见性,因此无论while循环里是什么,都能退出