變量可見性經常作爲大廠的面試題,難倒很多精通多線程併發的面試者,這裏列出這個題目儘可能多的答案。綜合考慮了變量可見性,線程上下文切換,以及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循環裏是什麼,都能退出