內(nèi)存模型

在Java內(nèi)存模型中,線程工作在自己的工作內(nèi)存,他會保留主存的變量拷貝。對于普通變量,為了保證執(zhí)行效率,在工作內(nèi)存中對變量的改變并不會立刻刷新到主存中中。
Volatile關(guān)鍵字
volatile的意思是易變的,不需要被優(yōu)化的。當一個變量被加上了這個關(guān)鍵字,就表示這個變量不需要被優(yōu)化、緩存。對該變量的修改會被立刻刷新到主存中,而當其他線程讀取該變量時,也會去主存中讀取新值。
使用場景
1.修飾線程中斷標志位
volatile boolean isCancel;
當我們希望通過標志位去結(jié)束一個正在運行的線程,那么應(yīng)該對該標志修飾volatile。
2.單例模式
public class Singleton {
private volatile static Singleton sInstance; //保證對象的可見性
private Singleton() {
}
public static Singleton getsInstance() {
if (sInstance == null) { //減少每次創(chuàng)建對象雙重鎖的開銷
synchronized (Singleton.class) {
if (sInstance == null) {
sInstance = new Singleton();
}
}
}
return sInstance;
}
}
問:為什么這里的volatile關(guān)鍵字是必要的?
答:因為創(chuàng)建對象不是一個原子操作。
禁止指令重排
假設(shè)創(chuàng)建一個對象需要3個步驟:
(1)分配內(nèi)存空間。
(2)初始化對象。
(3)將內(nèi)存空間的地址賦值給對應(yīng)的引用。
但是由于操作系統(tǒng)可以對指令進行重排序,所以上面的過程也可能會變成如下過程:
(1)分配內(nèi)存空間。
(2)將內(nèi)存空間的地址賦值給對應(yīng)的引用。
(3)初始化對象
如果采用后面的流程,可能導(dǎo)致引用不為null,但是對象還沒有初始化出來,其他的線程進行非空判斷后,會引用這個對象,導(dǎo)致錯誤。
volatile的原理和機制
下面這段話摘自《深入理解Java虛擬機》:
“觀察加入volatile關(guān)鍵字和沒有加入volatile關(guān)鍵字時所生成的匯編代碼發(fā)現(xiàn),加入volatile關(guān)鍵字時,會多出一個lock前綴指令”
lock前綴指令實際上相當于一個內(nèi)存屏障(也成內(nèi)存柵欄),內(nèi)存屏障會提供3個功能:
1.它確定指令重新排序時不會把其后的指令排在屏障之前,也不會把前面的指令排到屏障之后。即執(zhí)行到屏障指令時,其前面的指令已經(jīng)全部執(zhí)行完,且該屏障指令的執(zhí)行結(jié)果對后面的指令可見。
2.它會強制對緩存的修改操作立刻寫入主存
3.如果是讀取操作,它會導(dǎo)致其他線程中的緩存變量無效
synchronized關(guān)鍵字
要理解這個關(guān)鍵字的作用,就必須明白Java線程安全中的一個重要概念---原子性。
在Java中,對基本數(shù)據(jù)類型變量的讀取和賦值操作是原子性的。
如果要實現(xiàn)更大范圍的原子性操作,就必須用synchronized和Lock來實現(xiàn),他們能夠保證任一時刻只有一個線程執(zhí)行該代碼塊,從而保證了原子性。
當訪問臨界資源時,為了避免數(shù)據(jù)不一致,我們會采取同步操作,以確保在某一時刻只有一個線程在操作資源。
synchronized的用法
對象鎖
修飾對象方法或者明確指定某一個對象類鎖
修飾靜態(tài)方法或者鎖定.class
同一把鎖在某一時刻只能被一個線程持有,當另一個線程嘗試著獲取該鎖的時候,會陷入阻塞狀態(tài)。JVM維持了一個等待該鎖的隊列,一旦該鎖被某個線程釋放了,那么這個等待隊列中的線程會重新進入Ready(可運行)狀態(tài),等待調(diào)度器的下一次分配,進而去獲取需要的鎖。
無論synchronized關(guān)鍵字加在方法上還是對象上,如果它作用的對象是非靜態(tài)的,則它取得的鎖是對象;如果synchronized作用的對象是一個靜態(tài)方法或一個類,則它取得的鎖是類,該類所有的對象用同一把鎖。