近些天,學習模拟實作線程池,僅僅聽到線程兩個字,就讓人不寒而栗。剛開始接觸多線程程式設計,确實很難,多線程程式設計要結合很多很多的計算機底層知識,如作業系統,計算機組成原理等。這篇部落格,是我對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,然後線程結束。可是,事實并不是這樣, 來一起看一看結果。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICM38FdsYkRGZkRG9lcvx2bjxiNx8VZ6l2cs0TPR10MFRkT4lERPpHOsJGcohVYsR2MMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnLzQjM5AzMzQTM5ATMxgTMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
注意看,左邊的停止按鈕一直處于紅色狀态,說明程式一直在運作,如果你的電腦組態不是很好,你也可以明顯的聽到電腦風扇加速轉動的聲音。。。
我的了解:程式開始建立了三個線程,接下來的時間裡,三個線程開始并發執行。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 的,JVM 保證了每次讀變量都從記憶體中讀,跳過 CPU cache 這一步。
5、
volatile
會在生成的位元組碼中加入一條
lock
指令,這個指令有兩個作用:
- 将目前處理器緩存寫回到記憶體
- 使其它線程的cpu緩存失效
這樣其它線程再次讀取該變量的時候就會重新去記憶體中擷取,得到最新值。
經過以上閱讀,我們就可以對程式進行修改了。給goon增加volatile關鍵字,使goon的值從cpu緩存寫回到記憶體中(寫回到記憶體,讀取速度當然沒有在高速cache的讀取速度快)。這樣,當給goon指派為true時,就是把記憶體中的goon值改為true,要關閉線程時,就給goon指派為false時,即把記憶體中的goon改為false,這樣,在while循環執行的時候,是從記憶體中讀取goon的值,自然就不會造成死循環了。
來看結果:
可以清楚的看到,小紅按鈕變灰了!即線程結束了!!
以下是從彙編角度進行分析:
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的本質就是,禁止變量的寄存器優化!
如有錯誤,還請指點或評論。