問題
當多個線程併發同時進行set、get時,其它線程能否感知到flag的變化
public class ThreadSafeCache {
boolean flag = true;//默認設置true
public boolean isFlag() {
return flag;
}
public synchronized ThreadSafeCache setFlag(boolean flag) {
this.flag = flag;
return this;
}
public static void main(String[] args) {
ThreadSafeCache threadSafeCache = new ThreadSafeCache();
//循環創建多個線程
for (int i = 0;i < 10;i++){
new Thread(() -> {
int j = 0;
while(threadSafeCache.isFlag()){
j++;
}
System.out.println(j);
}).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
threadSafeCache.setFlag(false);
}
}
運行結果
可以看到程序是卡死了,一直沒有退出
分析
這個類非常簡單,裏面有一個屬性,有兩個方法,set、get,並且在set方法上添加了synchronized。
多線程併發的同時進行set、get操作,A線程調用set、B線程調用get能感知到flag發生變化嗎?
說到這裏,問題就變成了synchronized能否保證上下文可見性!!!
關鍵詞synchronized的用法
- 指定加鎖對象:對給定的對象進行加鎖,進入同步代碼前需要獲得給定對象的鎖。
- 直接作用於實例方法:相當於對當前對象的實例加鎖,進入同步代碼前需要獲得當前對象實例的鎖
- 直接作用於靜態方法:相當於對當前類進行加鎖,進入同步代碼前需要獲得當前類的鎖。
從代碼中,我們可以看到只對set方法加了同步鎖,多個線程調用set方法時,由於存在鎖,會一個一個的進行set,但對於get來說,並沒有加鎖,多個線程無需獲得該實例的鎖,就可以直接獲取到flag的值,那麼我們就需要考慮某一個線程set之後的flag對其它線程是否可見!!!
Java內存模型happens-before原則
JSR-133內存模型使用happens-before原則的概念來闡述操作之間的內存可見性。在JMM(JAVA Memory Model)中,如果一個執行的結果需要對另一個操作可見,那麼這兩個操作直接必須要存在happens-before關係。兩個操作可以是同一個線程內的也可以是不同線程中的。
happens-before(之前發生)原則
- 程序順序規則:一個線程中的每個操作,happens-before於該線程中的任意後續操作。
- 監視器鎖規則:對一個監視器的解鎖,happens-before於隨後對這個監視器的加鎖。
- volatile變量規則:對一個volatile域的寫,happens-before於任意後續對這個volatile的讀。
- 傳遞性:如果A happens-before B,且B happens-before C,那麼A happens-before C。
- 線程啓動規則:Thread對象的start方法先行發生於此線程的每一個動作。
- 線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生。
- 線程終結規則:線程中所有的操作都先行發生於線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值的手段檢測到線程是否已經終止執行。
- 對象終結規則:一個對象的初始化完成先行發生於他的finalize()方法的開始。
注意:兩個操作之間存在happens-before關係,並不一定前一個操作必須要在後一個操作執行!!!
happens-before僅僅要求前一個操作的執行結果對後一個操作可見,且前一個操作的執行順序排在後一個操作之前(因爲java虛擬機重排不相關的指令)。
volatile
volatile可見性
前面的happens-before原則中提到了volatile變量規則:對一個volatile域的寫,happens-before於任意後續對這個volatile域的讀。因此,volatile保證了多線程下的可見性!!!
volatile禁止內存重排序
下面是JMM針對編譯器制定的volatile重排序規則:
是否能重排序 | 第二個操作 | ||
第一個操作 | 普通讀/寫 | volatile讀 | volatile寫 |
普通讀/寫 | NO | ||
volatile讀 | NO | NO | NO |
volatile寫 | NO | NO |
通過上面的分析我們添加關鍵字volatile來試試
結論
多線程併發的同時進行set、get操作,A線程調用set方法,B線程並不一定能對這個改變可見,上面的代碼中,如果get也添加synchronized也是可見的,還是happens-before的監視器規則:對一個監視器的解鎖,happens-before於隨後對這個監視器的加鎖。只是volatile對比synchronized更輕量級,所以本例使用volatile,但是對於符合非原子操作i++這裏還是不行的,還得用synchronized。
不過使用volatile也會限制一些調優