這恐怕是Java面試最內卷的題,沒有之一

本資料來源Java性能優化

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章