JMM中工作記憶體和主記憶體的關係
Java執行時的資料區域分佈:
一、共享區域:
(1)方法區:儲存了每個類的資訊(包括類的名稱、方法資訊、欄位資訊)、靜態變數、常量以及編譯器編譯後的程式碼等。其中常量池就是在此區域:記錄了每一個類或介面的常量池的執行時表示形式,執行期間也可將新的常量放入執行時常量池中,比如String的intern方法。
(2)堆:jvm中最大的一片區域,所有例項物件的記憶體分配都在這裡進行劃分。當物件無法在此得到分配空間時,就會OutOfMemory。這部分空間也是Java垃圾收集器管理的主要區域。
二、執行緒私有:
(1)程式計數器:在JVM中,多執行緒是通過執行緒輪流切換來獲得CPU執行時間的,因此,在任一具體時刻,一個CPU的核心只會執行一條執行緒中的指令,因此,為了能夠使得每個執行緒都線上程切換後能夠恢復在切換之前的程式執行位置,每個執行緒都需要有自己獨立的程式計數器,並且不能互相被幹擾,否則就會影響到程式的正常執行次序。因此,可以這麼說,程式計數器是每個執行緒所私有的。同時,如果執行緒數量太多,對於cpu來說就需要頻繁切換執行緒,導致cpu佔有率上升。
(2)java棧:存放的是一個個的棧幀,每個棧幀對應一個被呼叫的方法,在棧幀中包括區域性變量表(Local Variables)、運算元棧(Operand Stack)、指向當前方法所屬的類的執行時常量池(執行時常量池的概念在方法區部分會談到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些額外的附加資訊。
當執行緒執行一個方法時,就會隨之建立一個對應的棧幀,並將建立的棧幀壓棧。當方法執行完畢之後,便會將棧幀出棧。因此可知,執行緒當前執行的方法所對應的棧幀必定位於Java棧的頂部。當程式設計時呼叫的棧的深度太深,有可能會導致無法建立棧幀,導致OutOfMermory。
(3)本地棧:類似Java棧,但存放但是native方法的棧幀。
執行緒/工作記憶體/主記憶體 三者的關係
一、每個執行緒都有一個獨立的工作記憶體,用於儲存執行緒私有的資料
二、Java記憶體模型中規定所有變數都儲存在主記憶體,主記憶體是共享記憶體區域,所有執行緒都可以訪問
三、執行緒對變數的操作(讀取賦值等)必須在工作記憶體中進行。(執行緒安全問題的根本原因)
(1)首先要將變數從主記憶體拷貝的自己的工作記憶體空間
(2)然後對變數進行操作,操作完成後再將變數寫回主記憶體
(3)不能直接操作主記憶體中的變數,工作記憶體中儲存著主記憶體中的變數副本拷貝
(4)因此不同的執行緒間無法訪問對方的工作記憶體,執行緒間的通訊(傳值)必須通過主記憶體來完成。
主記憶體和工作記憶體
一、主記憶體是在執行期間所有變數的存放區域,當工作記憶體是執行期間中某一執行緒獨立私有的記憶體存放區域
二、執行緒間無法訪問對方的工作記憶體空間,都是通過主記憶體交換來實現
三、主記憶體的變數在工作記憶體中的值是複製過去的副本,讀寫完成後重新整理主記憶體,這意味著主記憶體如果發生了改變,工作記憶體並無法獲得最新的結果
四、多個執行緒對一個共享變數進行修改時,都是對自己工作記憶體的副本進行操作,相互不可見。主記憶體最後得到的結果是不可預知的
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的變數後,是無法退出迴圈的。因為isStop這個變數線上程中從主記憶體讀取到副本後,一直使用的是工作記憶體的副本。
Synchronized和Volatile
由於執行緒執行時,對工作記憶體的變數進行操作時,都是副本。
產生了兩個問題:
1.如何使工作記憶體副本從主記憶體獲取最新的值?
2.如何使其副本是最新避免在寫入之前被其他執行緒修改?
在主執行緒中改變了isStop=true後
下面這段程式碼可以退出迴圈:
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迴圈中加入了System.out.println的語句,也會退出迴圈。
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的實現中,使用了Synchonized關鍵字獲取輸出物件的鎖。
一開始以為是Synchronized引起了工作記憶體從主記憶體重新整理最新的資料,感覺不太合理,後臺各種搜尋後找到比較靠譜的答案:
jvm有一個鎖優化原則那就是 : 粗化鎖。如果一系列的連續操作都對同一個物件反覆加鎖和解鎖,甚至加鎖操作是出現在迴圈體中的,那即使沒有執行緒競爭,頻繁地進行互斥同步操作也會導致不必要的效能損耗。 如果虛擬機器探測到有這樣一串零碎的操作都對同一個物件加鎖,將會把加鎖同步的範圍擴充套件(膨脹)到整個操作序列的外部(由多次加鎖程式設計只加鎖一次)。
====更新===
上面粗化鎖的解釋應該也不準確,鎖的物件並不是變數isStop,並不能將isStop加上一個執行緒可見的屬性。
在synchronized鎖操作中,由於是一個耗時操作,jvm會盡力在cpu空閒時間去主記憶體同步工作記憶體中變數的值。
====更新===
同樣下面的程式碼,也能退出迴圈:
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();
發現這個迴圈體裡面並沒有synchronized的關鍵字,猜測只能是jvm線上程有空閒的時間盡力的去主存取最新資料。
引入Synchronized後,同步程式碼塊在迴圈體內,synchronized保證原子性和有序性,可見性。
為了保證不同執行緒間可以獲得同一個共享變數,Volatile也可以做到。
volatile變數在每次被執行緒訪問時,都強迫從主記憶體中讀該變數的值,而當該變數發生變化時,又會強迫將最新的值重新整理到主記憶體,任何時刻,不同的執行緒總是能夠看到該變數的最新值。
上面程式碼中,對isStop用volatile修飾,也可以退出執行緒。