關於多個執行緒同時呼叫單例模式的物件,該物件中方法的區域性變數是否會受多個執行緒的影響

人生須臾發表於2017-03-15
關於多個執行緒同時呼叫單例模式的物件,該物件中方法的區域性變數是否會受多個執行緒的影響
對於那些會以多執行緒執行的單例類,例如Web應用中的Servlet,每個方法中對區域性變數的操作都是線上程自己獨立的記憶體區域內完成的,所以是執行緒安全的。
對於成員變數的操作,可以使用ThreadLocal來保證執行緒安全。

區域性變數不會受多執行緒影響
成員變數會受到多執行緒影響

多個執行緒應該是呼叫的同一個物件的同一個方法:
如果方法裡無成員變數,那麼不受任何影響
如果方法裡有成員變數,只有讀操作,不受影響
                      存在寫操作,考慮多執行緒影響值



深入Java核心:JVM中的棧和區域性變數

在Java程式中,每當啟用一個執行緒時,JVM就為他分配一個Java棧,棧是以幀為單位儲存當前執行緒的執行狀態。今天我們繼續深入Java核心,探祕JVM中的棧和區域性變數。

Java開發中,每當我們在程式中使用new生成一個物件,物件的引用存放在棧裡,而物件是存放在堆裡的。可以看出棧在Java核心的重要位置。今天我們就繼續深入Java核心這個系列,為您介紹Java中的棧、區域性變數及其之間的關係。

深入Java核心:Java記憶體分配原理精講  探祕Java垃圾回收機制  Java中多型的實現機制 

Java中的棧

每當啟用一個執行緒時,JVM就為他分配一個Java棧,棧是以幀為單位儲存當前執行緒的執行狀態。某個執行緒正在執行的方法稱為當前方法,當前方法使用的棧幀稱為當前幀,當前方法所屬的類稱為當前類,當前類的常量池稱為當前常量池。當執行緒執行一個方法時,它會跟蹤當前常量池。

每當執行緒呼叫一個Java方法時,JVM就會在該執行緒對應的棧中壓入一個幀,這個幀自然就成了當前幀。當執行這個方法時,它使用這個幀來儲存引數、區域性變數、中間運算結果等等。

Java棧上的所有資料都是私有的。任何執行緒都不能訪問另一個執行緒的棧資料。所以我們不用考慮多執行緒情況下棧資料訪問同步的情況。

像方法區和堆一樣,Java棧和幀在記憶體中也不必是連續的,幀可以分佈在連續的棧裡,也可以分佈在堆裡

Java棧的組成元素——棧幀

棧幀由三部分組成:區域性變數區、運算元棧、幀資料區。區域性變數區和運算元棧的大小要視對應的方法而定,他們是按字長計算的。但呼叫一個方法時,它從型別資訊中得到此方法區域性變數區和運算元棧大小,並據此分配棧記憶體,然後壓入Java棧。

區域性變數區 區域性變數區被組織為以一個字長為單位、從0開始計數的陣列,型別為short、byte和char的值在存入陣列前要被轉換成int值,而long和 double在陣列中佔據連續的兩項,在訪問區域性變數中的long或double時,只需取出連續兩項的第一項的索引值即可,如某個long值在區域性變數區中佔據的索引時3、4項,取值時,指令只需取索引為3的long值即可。

下面就看個例子,好讓大家對區域性變數區有更深刻的認識。這個圖來自《深入JVM》:
public static int runClassMethod(int i,long l,float f,double d,Object o,byte b) {   
          return 0; 
        
}                  
public int runInstanceMethod(char c,double d,short s,boolean b) { 
            
         return 0; 
       
 }    

上面程式碼片的方法引數和區域性變數在區域性變數區中的儲存結構如下圖:

上面這個圖沒什麼好說的,大家看看就會懂。但是,在這個圖裡,有一點需要注意:

runInstanceMethod的區域性變數區第一項是個reference(引用),它指定的就是物件本身的引用,也就是我們常用的this,但是在runClassMethod方法中,沒這個引用,那是因為runClassMethod是個靜態方法。

運算元棧和區域性變數區一樣,運算元棧也被組織成一個以字長為單位的陣列。但和前者不同的是,它不是通過索引來訪問的,而是通過入棧和出棧來訪問的。可把運算元棧理解為儲存計算時,臨時資料的儲存區域。下面我們通過一段簡短的程式片段外加一幅圖片來了解下運算元棧的作用。

int a = 100;

int b = 98;

int c = a+b;

從圖中可以得出:運算元棧其實就是個臨時資料儲存區域,它是通過入棧和出棧來進行操作的。

幀資料區除了區域性變數區和運算元棧外,Java棧幀還需要一些資料來支援常量池解析、正常方法返回以及異常派發機制。這些資料都儲存在Java棧幀的幀資料區中。
當JVM執行到需要常量池資料的指令時,它都會通過幀資料區中指向常量池的指標來訪問它。

除了處理常量池解析外,幀裡的資料還要處理Java方法的正常結束和異常終止。如果是通過return正常結束,則當前棧幀從Java棧中彈出,恢復發起呼叫的方法的棧。如果方法又返回值,JVM會把返回值壓入到發起呼叫方法的運算元棧。

為了處理Java方法中的異常情況,幀資料區還必須儲存一個對此方法異常引用表的引用。當異常丟擲時,JVM給catch塊中的程式碼。如果沒發現,方法立即終止,然後JVM用幀區資料的資訊恢復發起呼叫的方法的幀。然後再發起呼叫方法的上下文重新丟擲同樣的異常。

棧的整個結構

在前面就描述過:棧是由棧幀組成,每當執行緒呼叫一個Java方法時,JVM就會在該執行緒對應的棧中壓入一個幀,而幀是由區域性變數區、運算元棧和幀資料區組成。那在一個程式碼塊中,棧到底是什麼形式呢?下面是我從《深入JVM》中摘抄的一個例子,大家可以看看:

程式碼片段:

執行過程中的三個快照:

上面所給的圖,只想說明兩件事情,我們也可用此來理解Java中的棧:

1、只有在呼叫一個方法時,才為當前棧分配一個幀,然後將該幀壓入棧。

2、幀中儲存了對應方法的區域性資料,方法執行完,對應的幀則從棧中彈出,並把返回結果儲存在呼叫方法的幀的運算元棧中。

相關文章