天天看點

從彙編角度分析volatile關鍵字

        近些天,學習模拟實作線程池,僅僅聽到線程兩個字,就讓人不寒而栗。剛開始接觸多線程程式設計,确實很難,多線程程式設計要結合很多很多的計算機底層知識,如作業系統,計算機組成原理等。這篇部落格,是我對volatile關鍵字的一個初探。

首先,開始的代碼是這樣的(代碼很簡單,很短,是以,可以認真看看)

ThreadOperate類,我自己定義的一個類,實作了Runnable接口

public class ThreadOperate implements Runnable {
    private static int id;
    private boolean goon;
    private int currentId;

    public ThreadOperate() {
        goon = true;
        currentId = ++id;
        new Thread(this, "Thread-" + currentId).start();
    }

    public void closeThread() {
        System.out.println("收到結束線程【" + Thread.currentThread().getName() + "】的指令");
        goon = false;
    }

    @Override
    public void run() {
        System.out.println("線程【" + Thread.currentThread().getName() + "】開始執行");
        while (goon) {
            int sum = 0;
	    int i;

            for (i = 0; i < 10; i++) {
		sum += i;
            }
	    i = sum;
        }
        System.out.println("線程【" + Thread.currentThread().getName() + "】已經結束!");
    }
}
           

 Test類:

public class Test {
    public static void main(String[] args) {
//        建立三個線程
        ThreadOperate[] to = new ThreadOperate[3];
        for (int i = 0; i < to.length; i++) {
            to[i] = new ThreadOperate();
            System.out.println(i + "---" + to[i].hashCode());
        }

//        結束線程
        for (int i = 0; i < to.length; i++) {
            try {
                Thread.sleep(2000);
                System.out.println("準備結束【" + i + "---" + to[i].hashCode() + "】線程");
                to[i].closeThread();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
           

 看吧,看完代碼後,不妨結合自己對線程的了解,猜一猜程式的執行結果是什麼樣的?我剛開始認為,建立3個線程,令goon等于false,然後線程結束。可是,事實并不是這樣, 來一起看一看結果。

從彙編角度分析volatile關鍵字

 注意看,左邊的停止按鈕一直處于紅色狀态,說明程式一直在運作,如果你的電腦組態不是很好,你也可以明顯的聽到電腦風扇加速轉動的聲音。。。

我的了解:程式開始建立了三個線程,接下來的時間裡,三個線程開始并發執行。run()方法裡面的while(goon)一直死循環執行,通過new ThreadOperate()操作,使goon為true,但goon沒有volatile關鍵字,會被編譯器優化,goon的值被存到cpu緩存裡面去。即每個線程讀取goon變量的值時,都會從cpu緩存裡面讀取,是以,讀取到的goon值永遠都是true,是以會一直死循環,線程不會結束。

那麼,到底如何解決這個問題呢,說實話,很簡單,隻需要給goon加上volatile關鍵字即可。所有,我們一起來看一看volatile關鍵字,以下内容為我百度整理而來。

volatile關鍵字

1、volatile定義的變量,用來確定将變量的更新操作通知到其他線程。當把變量聲明為volatile類型後,編譯器與運作時都會注意到這個變量是共享的,是以不會将該變量上的操作與其他記憶體操作一起重排序。volatile變量不會被緩存在寄存器或者對其他處理器不可見的地方,是以在讀取volatile類型的變量時總會傳回最新寫入的值。

2、一旦一個共享變量(類的成員變量、類的靜态成員變量)被volatile修飾之後,那麼就具備了兩層語義:

  1)保證了不同線程對這個變量進行操作時的可見性,可見性是指,在多線程環境,共享變量的操作對于每個線程來說,都是記憶體可見的,也就是每個線程擷取的volatile變量都是最新值;并且每個線程對volatile變量的修改,都直接重新整理到主存。

  2)禁止進行指令重排序。

3、在通路volatile變量時不會執行加鎖操作,是以也就不會使執行線程阻塞,是以volatile變量是一種比sychronized關鍵字更輕量級的同步機制。

4、當對非 volatile 變量進行讀寫的時候,每個線程先從記憶體拷貝變量到CPU緩存中。如果計算機有多個CPU,每個線程可能在不同的CPU上被處理,這意味着每個線程可以拷貝到不同的 CPU cache 中。當一個共享變量被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,它會去記憶體中讀取新值。如下圖

從彙編角度分析volatile關鍵字

而聲明變量是 volatile 的,JVM 保證了每次讀變量都從記憶體中讀,跳過 CPU cache 這一步。

5、

volatile

會在生成的位元組碼中加入一條

lock

指令,這個指令有兩個作用:

  1. 将目前處理器緩存寫回到記憶體
  2. 使其它線程的cpu緩存失效

這樣其它線程再次讀取該變量的時候就會重新去記憶體中擷取,得到最新值。

經過以上閱讀,我們就可以對程式進行修改了。給goon增加volatile關鍵字,使goon的值從cpu緩存寫回到記憶體中(寫回到記憶體,讀取速度當然沒有在高速cache的讀取速度快)。這樣,當給goon指派為true時,就是把記憶體中的goon值改為true,要關閉線程時,就給goon指派為false時,即把記憶體中的goon改為false,這樣,在while循環執行的時候,是從記憶體中讀取goon的值,自然就不會造成死循環了。

來看結果:

從彙編角度分析volatile關鍵字

可以清楚的看到,小紅按鈕變灰了!即線程結束了!!

以下是從彙編角度進行分析:

while (goon) {
    int sum = 0;
    int i;

    for (i = 0; i < 10; i++) {
        sum += i;
    }
    i = sum;
}
           

假設i這個局部變量的偏移量為-4,i的首位址應該是edp[-4]。

i++對應的彙編語言:

mov cx, edp[-4]

loop:

inc cx

cmp cx, 10

jl loop:

可以看出,每次都是i的首位址對應的值和10比較。i這個局部變量并沒有做任何的操作,寄存器cx隻是接收了i的首位址,然後就自己玩去了,根本不管i的死活。

如果對變量i增加volitale關鍵字,則,将使用下面的彙編:

loop:

mov ecx, edp[-4] 

inc ecx

mov edp[-4], ecx

cmp ecx, 10   

jl loop

以上代碼可以了解為這樣的:

ecx = i;

++ecx;

i = ecx;

if (ecx < 10) goto loop:

從彙編的角度,可以對volatile關鍵字進行更深一步的了解。

volatile的本質就是,禁止變量的寄存器優化!

如有錯誤,還請指點或評論。