Java的內存模型:
堆區(qū):
- 存儲的全部是對象设江,每個對象都包含一個與之對應的class的信息备畦。(class的目的是得到操作指令)疟羹。
- JVM的堆區(qū)(heap)被所有線程共享(相對于棧區(qū)卧抗,棧區(qū)的數據不共享)啊掏,堆中不存放基本類型和對象引用,只存放對象本身禾唁。
棧區(qū):
- 每個線程包含一個棧區(qū)效览,棧中只保存基礎數據類型的對象和自定義對象的引用(不是對象),對象都存放在堆區(qū)中荡短。
- 每個棧中的數據(原始類型和對象引用)都是私有的丐枉,其他棧不能訪問。
- 棧分為3個部分:基本類型變量區(qū)掘托、執(zhí)行環(huán)境上下文瘦锹、操作指令區(qū)(存放操作指令)。
方法區(qū):
- 又叫靜態(tài)區(qū)闪盔,跟堆一樣弯院,被所有的線程共享。方法區(qū)包含所有的class和static變量泪掀。
- 方法區(qū)中包含的都是在整個程序中永遠唯一的元素听绳,如class,static變量异赫。
方法調用棧:
在Java虛擬機進程中椅挣,每個線程都會擁有一個方法調用棧,用來跟蹤線程運行中一系列的方法調用過程祝辣,棧中的每一個元素就被稱為棧幀贴妻,每當線程調用一個方法的時候就會向方法棧壓入一個新幀。這里的幀用來存儲方法的參數蝙斜、局部變量和運算過程中的臨時數據名惩。
每個線程都有自己的棧內存,用于存儲本地變量孕荠,方法參數和棧調用娩鹉,一個線程中存儲的變量對其它線程是不可見的攻谁。而堆是所有線程共享的一片公用內存區(qū)域。對象都在堆里創(chuàng)建弯予,為了提升效率線程會從堆中弄一個緩存到自己的棧戚宦,如果多個線程使用該變量就可能引發(fā)問題,這時 volatile 變量就可以發(fā)揮作用了锈嫩,它要求線程從主存中讀取變量的值受楼。
Java中堆和棧有什么不同?
為什么把這個問題歸類在多線程和并發(fā)面試題里呼寸?因為棧是一塊和線程緊密相關的內存區(qū)域艳汽。每個線程都有自己的棧內存,用于存儲本地變量对雪,方法參數和棧調用河狐,一個線程中存儲的變量對其它線程是不可見的。而堆是所有線程共享的一片公用內存區(qū)域瑟捣。對象都在堆里創(chuàng)建馋艺,為了提升效率線程會從堆中弄一個緩存到自己的棧,如果多個線程使用該變量就可能引發(fā)問題迈套,這時volatile 變量就可以發(fā)揮作用了捐祠,它要求線程從主存中讀取變量的值。
線程封閉技術
如果在單線程內訪問數據交汤,就不需要同步雏赦。這種技術被稱為線程封閉。
Java提供了一些機制來維持線程封閉性芙扎,例如局部變量和ThreadLocal類。
1)局部變量:
因為每個線程都有自己的方法調用棧填大,并且是私有的戒洼,所以訪問方法局部變量無需同步(其他線程并發(fā)執(zhí)行同一個方法,得到的局部變量也不是同一個)
2)ThreadLocal類:
ThreadLocal是Java里一種特殊的變量允华。每個線程都有一個ThreadLocal就是每個線程都擁有了自己獨立的一個變量圈浇,競爭條件被徹底消除了。它是為創(chuàng)建代價高昂的對象獲取線程安全的好方法靴寂,比如你可以用ThreadLocal讓SimpleDateFormat變成線程安全的磷蜀,因為那個類創(chuàng)建代價高昂且每次調用都需要創(chuàng)建不同的實例所以不值得在局部范圍使用它,如果為每個線程提供一個自己獨有的變量拷貝百炬,將大大提高效率褐隆。首先,通過復用減少了代價高昂的對象的創(chuàng)建個數剖踊。其次庶弃,你在沒有使用高代價的同步或者不變性的情況下獲得了線程安全衫贬。線程局部變量的另一個不錯的例子是ThreadLocalRandom類,它在多線程環(huán)境中減少了創(chuàng)建代價高昂的Random對象的個數歇攻。
DCL模式實現單例為什么并不能保證線程安全
DCL模式實現單例固惯,如下所示:
private static UserSingleton sInstance;
private UserSingleton () {
}
public static UserSingleton getInstance() {
if (sInstance == null) {
synchornized(UserSingleton.class){
if (sInstance == null) {
sInstance = new UserSingleton();
}
}
}
return sInstance;
}
因為在實例化對象這個地方new UserSingleton();這里是實際上在JVM中進行三步指令操作(1、分配內存 2缴守、執(zhí)行構造函數并執(zhí)行實例化變量 3葬毫、分配引用),而JVM對代碼編譯時會進行性能優(yōu)化而對指令進行重排屡穗,因此123可能被優(yōu)化成132贴捡,因此如果單例變量不是volatile修飾的,那么可能在并發(fā)獲取單例的情況下鸡捐,一個對象進入同步代碼塊栈暇,進行實例化,走到指令132步驟中的3時箍镜,此時指令2并未執(zhí)行源祈,但sInstance已經不為null了(已經分配了引用),另外一個線程查詢到sInstance已經不為null返回單例給客戶端色迂,客戶端使用未初始化完成的單例進行操作香缺,這時候會出現變量初始值不對引起的問題了。
解決辦法
單例變量使用volatile關鍵字修飾歇僧,即:
private static volatile UserSingleton sInstance;
volatile關鍵字可以保證變量的可見性图张,因為對volatile的操作都在Main Memory中,而Main Memory是被所有線程所共享的诈悍,這里的代價就是犧牲了性能祸轮,無法利用寄存器或Cache,因為它們都不是全局的侥钳,無法保證可見性适袜,可能產生臟讀。
volatile還有一個作用就是局部阻止重排序的發(fā)生舷夺,對volatile變量的操作指令都不會被重排序苦酱,因為如果重排序,又可能產生可見性問題给猾。
在保證可見性方面疫萤,鎖(包括顯式鎖、對象鎖)以及對原子變量的讀寫都可以確保變量的可見性敢伸。但是實現方式略有不同扯饶,例如同步鎖保證得到鎖時從內存里重新讀入數據刷新緩存,釋放鎖時將數據寫回內存以保數據可見,而volatile變量干脆都是讀寫內存帝际。
競態(tài)條件為什么不能通過volatile消除
volatile只保證可見性蔓同,不保證原子性,原子變量&加鎖機制可以保證可見性和原子性蹲诀。
volatile與各類原子類區(qū)別:
volatile關鍵字只保證可見性斑粱,不保證原子性,它保證多個線程對volatile修飾的變量的寫操作會排在讀操作之前脯爪,但并不保證它修飾的變量操作具有原子性则北,例如volatile的count++依然不是原子操作,多個線程并發(fā)依然會出現競態(tài)條件痕慢,但AtomInteger可以使用getAndIncrement保證為原子操作尚揣,因此如果需要實現一個支持并發(fā)的計數器,不能使用以下代碼
private volatile int count;
count++;
而是要使用:
private AtomInteger mCount = new AtomInteger();
mCount.getAndIncrement();
參考來源:
JVM 內存初學 堆(heap)掖举、棧(stack)和方法區(qū)(method)
《Java并發(fā)編程實踐》