Java運行時的數(shù)據(jù)區(qū)域分布:
一有序、共享區(qū)域:
(1)方法區(qū):存儲了每個類的信息(包括類的名稱抹腿、方法信息、字段信息)旭寿、靜態(tài)變量警绩、常量以及編譯器編譯后的代碼等。其中常量池就是在此區(qū)域:記錄了每一個類或接口的常量池的運行時表示形式盅称,運行期間也可將新的常量放入運行時常量池中肩祥,比如String的intern方法。
(2)堆:jvm中最大的一片區(qū)域微渠,所有實例對象的內(nèi)存分配都在這里進行劃分。當(dāng)對象無法在此得到分配空間時咧擂,就會OutOfMemory逞盆。這部分空間也是Java垃圾收集器管理的主要區(qū)域。
二松申、線程私有:
(1)程序計數(shù)器:在JVM中云芦,多線程是通過線程輪流切換來獲得CPU執(zhí)行時間的,因此贸桶,在任一具體時刻舅逸,一個CPU的內(nèi)核只會執(zhí)行一條線程中的指令,因此皇筛,為了能夠使得每個線程都在線程切換后能夠恢復(fù)在切換之前的程序執(zhí)行位置琉历,每個線程都需要有自己獨立的程序計數(shù)器,并且不能互相被干擾水醋,否則就會影響到程序的正常執(zhí)行次序旗笔。因此,可以這么說拄踪,程序計數(shù)器是每個線程所私有的蝇恶。同時,如果線程數(shù)量太多惶桐,對于cpu來說就需要頻繁切換線程撮弧,導(dǎo)致cpu占有率上升潘懊。
(2)java棧:存放的是一個個的棧幀,每個棧幀對應(yīng)一個被調(diào)用的方法贿衍,在棧幀中包括局部變量表(Local Variables)授舟、操作數(shù)棧(Operand Stack)、指向當(dāng)前方法所屬的類的運行時常量池(運行時常量池的概念在方法區(qū)部分會談到)的引用(Reference to runtime constant pool)舌厨、方法返回地址(Return Address)和一些額外的附加信息岂却。
當(dāng)線程執(zhí)行一個方法時,就會隨之創(chuàng)建一個對應(yīng)的棧幀裙椭,并將建立的棧幀壓棧躏哩。當(dāng)方法執(zhí)行完畢之后,便會將棧幀出棧揉燃。因此可知扫尺,線程當(dāng)前執(zhí)行的方法所對應(yīng)的棧幀必定位于Java棧的頂部。當(dāng)程序設(shè)計時調(diào)用的棧的深度太深炊汤,有可能會導(dǎo)致無法創(chuàng)建棧幀正驻,導(dǎo)致OutOfMermory。
(3)本地棧:類似Java棧抢腐,但存放但是native方法的棧幀姑曙。
線程/工作內(nèi)存/主內(nèi)存 三者的關(guān)系
一、每個線程都有一個獨立的工作內(nèi)存迈倍,用于存儲線程私有的數(shù)據(jù)
二伤靠、Java內(nèi)存模型中規(guī)定所有變量都存儲在主內(nèi)存,主內(nèi)存是共享內(nèi)存區(qū)域啼染,所有線程都可以訪問
三宴合、線程對變量的操作(讀取賦值等)必須在工作內(nèi)存中進行。(線程安全問題的根本原因)
(1)首先要將變量從主內(nèi)存拷貝的自己的工作內(nèi)存空間
(2)然后對變量進行操作迹鹅,操作完成后再將變量寫回主內(nèi)存
(3)不能直接操作主內(nèi)存中的變量卦洽,工作內(nèi)存中存儲著主內(nèi)存中的變量副本拷貝
(4)因此不同的線程間無法訪問對方的工作內(nèi)存,線程間的通信(傳值)必須通過主內(nèi)存來完成斜棚。
主內(nèi)存和工作內(nèi)存
一阀蒂、主內(nèi)存是在運行期間所有變量的存放區(qū)域,當(dāng)工作內(nèi)存是運行期間中某一線程獨立私有的內(nèi)存存放區(qū)域
二弟蚀、線程間無法訪問對方的工作內(nèi)存空間脂新,都是通過主內(nèi)存交換來實現(xiàn)
三、主內(nèi)存的變量在工作內(nèi)存中的值是復(fù)制過去的副本粗梭,讀寫完成后刷新主內(nèi)存争便,這意味著主內(nèi)存如果發(fā)生了改變,工作內(nèi)存并無法獲得最新的結(jié)果
四断医、多個線程對一個共享變量進行修改時滞乙,都是對自己工作內(nèi)存的副本進行操作奏纪,相互不可見。主內(nèi)存最后得到的結(jié)果是不可預(yù)知的
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread1 is start斩启!");
boolean wIsStop = false;
while (!isStop) {
}
System.out.println("thread1 is going to stop!");
}
}).start();
上面這段代碼中序调,主線程改變isStop的變量后,是無法退出循環(huán)的兔簇。因為isStop這個變量在線程中從主內(nèi)存讀取到副本后发绢,一直使用的是工作內(nèi)存的副本。
Synchronized和Volatile
由于線程運行時垄琐,對工作內(nèi)存的變量進行操作時边酒,都是副本。
產(chǎn)生了兩個問題:
1.如何使工作內(nèi)存副本從主內(nèi)存獲取最新的值狸窘?
2.如何使其副本是最新避免在寫入之前被其他線程修改墩朦?
在主線程中改變了isStop=true后
下面這段代碼可以退出循環(huán):
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread2 is start!");
while (!isStop) {
synchronized(String.class) {
}
}
System.out.println("thread2 is going to stop!");
System.out.println("break count: " + count);
}
}).start();
或者是在while循環(huán)中加入了System.out.println的語句翻擒,也會退出循環(huán)氓涣。
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread2 is start!");
while (!isStop) {
System.out.println("thread2 stopFlag:" + isStop);
}
System.out.println("thread2 is going to stop!");
System.out.println("break count: " + count);
}
}).start();
其實System.out.println的實現(xiàn)中陋气,使用了Synchonized關(guān)鍵字獲取輸出對象的鎖劳吠。
一開始以為是Synchronized引起了工作內(nèi)存從主內(nèi)存刷新最新的數(shù)據(jù),感覺不太合理巩趁,后臺各種搜索后找到比較靠譜的答案:
jvm有一個鎖優(yōu)化原則那就是 : 粗化鎖痒玩。如果一系列的連續(xù)操作都對同一個對象反復(fù)加鎖和解鎖,甚至加鎖操作是出現(xiàn)在循環(huán)體中的晶渠,那即 使沒有線程競爭凰荚,頻繁地進行互斥同步操作也會導(dǎo)致不必要的性能損耗燃观。 如果虛擬機探測到有這樣一串零碎的操作都對同一個對象加鎖褒脯,將會把加鎖同步的范圍擴展(膨脹)到整個操作序列的外部(由多次加鎖編程只加鎖一次)。
====更新===
上面粗化鎖的解釋應(yīng)該也不準(zhǔn)確缆毁,鎖的對象并不是變量isStop番川,并不能將isStop加上一個線程可見的屬性。
在synchronized鎖操作中脊框,由于是一個耗時操作颁督,jvm會盡力在cpu空閑時間去主內(nèi)存同步工作內(nèi)存中變量的值。
====更新===
同樣下面的代碼浇雹,也能退出循環(huán):
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread3 is start沉御!");
while (!isStop) {
try {
Thread.sleep(1);
}catch (Exception e) {
e.printStackTrace();
}
}
System.out.println("thread3 is going to stop!");
}
}).start();
發(fā)現(xiàn)這個循環(huán)體里面并沒有synchronized的關(guān)鍵字,猜測只能是jvm在線程有空閑的時間盡力的去主存取最新數(shù)據(jù)昭灵。
引入Synchronized后吠裆,同步代碼塊在循環(huán)體內(nèi)伐谈,synchronized保證原子性和有序性,可見性试疙。
為了保證不同線程間可以獲得同一個共享變量诵棵,Volatile也可以做到。
volatile變量在每次被線程訪問時祝旷,都強迫從主內(nèi)存中讀該變量的值履澳,而當(dāng)該變量發(fā)生變化時,又會強迫將最新的值刷新到主內(nèi)存怀跛,任何時刻距贷,不同的線程總是能夠看到該變量的最新值。
上面代碼中敌完,對isStop用volatile修飾储耐,也可以退出線程。