[深入理解Java虛擬機器]第十三章 執行緒安全與鎖優化-執行緒安全

Coding-lover發表於2015-11-08

概述

在軟體業發展的初期,程式編寫都是以演算法為核心的,程式設計師會把資料和過程分別作為獨立的部分來考慮,資料代表問題空間中的客體,程式程式碼則用於處理這些資料,這種思維方式直接站在計算機的角度去抽象問題和解決問題,稱為程式導向的程式設計思想。與此相對的是,物件導向的程式設計思想是站在現實世界的角度去抽象和解決問題,它把資料和行為都看做是物件的一部分,這樣可以讓程式設計師能以符合現實世界的思維方式來編寫和組織程式。

程式導向的程式設計思想極大地提升了現代軟體開發的生產效率和軟體可以達到的規模,但是現實世界與計算機世界之間不可避免地存在一些差異。例如,人們很難想象現實中的物件在一項工作進行期間,會被不停地中斷和切換,物件的屬性(資料)可能會在中斷期間被修改和變“髒”,而這些事件在計算機世界中則是很正常的事情。有時候,良好的設計原則不得不向現實做出一些讓步,我們必須讓程式在計算機中正確無誤地執行,然後再考慮如何將程式碼組織得更好,讓程式執行得更快。對於這部分的主題“高效併發”來講,首先需要保證併發的正確性,然後在此基礎上實現高效。本章先從如何保證併發的正確性和如何實現執行緒安全講起。

執行緒安全

“執行緒安全”這個名稱,相信稍有經驗的程式設計師都會聽說過,甚至在程式碼編寫和走查的時候可能還會經常掛在嘴邊,但是如何找到一個不太拗口的概念來定義執行緒安全卻不是一件容易的事情,在Google中搜尋它的概念,找到的是類似於“如果一個物件可以安全地被多個執行緒同時使用,那它就是執行緒安全的”這樣的定義——並不能說它不正確,但是人們無法從中獲取到任何有用的資訊。

《Java Concurrency In Practice》的作者Brian Goetz對“執行緒安全”有一個比較恰當的定義:“當多個執行緒訪問一個物件時,如果不用考慮這些執行緒在執行時環境下的排程和交替執行,也不需要進行額外的同步,或者在呼叫方進行任何其他的協調操作,呼叫這個物件的行為都可以獲得正確的結果,那這個物件是執行緒安全的”。

這個定義比較嚴謹,它要求執行緒安全的程式碼都必須具備一個特徵:程式碼本身封裝了所有必要的正確性保障手段(如互斥同步等),令呼叫者無須關心多執行緒的問題,更無須自己採取任何措施來保證多執行緒的正確呼叫。這點聽起來簡單,但其實並不容易做到,在大多數場景中,我們都會將這個定義弱化一些,如果把“呼叫這個物件的行為”限定為“單次呼叫”,這個定義的其他描述也能夠成立的話,我們就可以稱它是執行緒安全了,為什麼要弱化這個定義,現在暫且放下,稍後再詳細探討。

Java語言中的執行緒安全

我們已經有了執行緒安全的一個抽象定義,那接下來就討論一下在Java語言中,執行緒安全具體是如何體現的?有哪些操作是執行緒安全的?我們這裡討論的執行緒安全,就限定於多個執行緒之間存在共享資料訪問這個前提,因為如果一段程式碼根本不會與其他執行緒共享資料,那麼從執行緒安全的角度來看,程式是序列執行還是多執行緒執行對它來說是完全沒有區別的。

為了更加深入地理解執行緒安全,在這裡我們可以不把執行緒安全當做一個非真即假的二元排他選項來看待,按照執行緒安全的“安全程度”由強至弱來排序,我們可以將Java語言中各種操作共享的資料分為以下5類:不可變、絕對執行緒安全、相對執行緒安全、執行緒相容和執行緒對立。

1.不可變

在Java語言中(特指JDK 1.5以後,即Java記憶體模型被修正之後的Java語言),不可變(Immutable)的物件一定是執行緒安全的,無論是物件的方法實現還是方法的呼叫者,都不需要再採取任何的執行緒安全保障措施,只要一個不可變的物件被正確地構建出來(沒有發生this引用逃逸的情況),那其外部的可見狀態永遠也不會改變,永遠也不會看到它在多個執行緒之中處於不一致的狀態。“不可變”帶來的安全性是最簡單和最純粹的。

Java語言中,如果共享資料是一個基本資料型別,那麼只要在定義時使用final關鍵字修飾它就可以保證它是不可變的。如果共享資料是一個物件,那就需要保證物件的行為不會對其狀態產生任何影響才行,不妨想一想java.lang.String類的物件,它是一個典型的不可變物件,我們呼叫它的substring()、replace()和concat()這些方法都不會影響它原來的值,只會返回一個新構造的字串物件。

保證物件行為不影響自己狀態的途徑有很多種,其中最簡單的就是把物件中帶有狀態的變數都宣告為final,這樣在建構函式結束之後,它就是不可變的,例如程式碼清單13-1中java.lang.Integer建構函式所示的,它通過將內部狀態變數value定義為final來保障狀態不變。

程式碼清單13-1 JDK中Integer類的建構函式

/**
*The value of the<code>Integer</code>.
*@serial
*/
private final int value;
/**
*Constructs a newly allocated<code>Integer</code>object that
*represents the specified<code>int</code>value.
*
*@param value the value to be represented by the
*<code>Integer</code>object.
*/
public Integer(int value){
    this.value=value;
}

在Java API中符合不可變要求的型別,除了上面提到的String之外,常用的還有列舉型別,以及java.lang.Number的部分子類,如Long和Double等數值包裝型別,BigInteger和BigDecimal等大資料型別;但同為Number的子型別的原子類AtomicInteger和AtomicLong則並非不可變的,讀者不妨看看這兩個原子類的原始碼,想一想為什麼。

2.絕對執行緒安全

絕對的執行緒安全完全滿足Brian Goetz給出的執行緒安全的定義,這個定義其實是很嚴格的,一個類要達到“不管執行時環境如何,呼叫者都不需要任何額外的同步措施”通常需要付出很大的,甚至有時候是不切實際的代價。在Java API中標註自己是執行緒安全的類,大多數都不是絕對的執行緒安全。我們可以通過Java API中一個不是“絕對執行緒安全”的執行緒安全類來看看這裡的“絕對”是什麼意思。

如果說java.util.Vector是一個執行緒安全的容器,相信所有的Java程式設計師對此都不會有異議,因為它的add()、get()和size()這類方法都是被synchronized修飾的,儘管這樣效率很低,但確實是安全的。但是,即使它所有的方法都被修飾成同步,也不意味著呼叫它的時候永遠都不再需要同步手段了,請看一下程式碼清單13-2中的測試程式碼。

程式碼清單13-2 對Vector執行緒安全的測試

 private static Vector<Integer> vector = new Vector<Integer>();

    public static void main(String[] args) {
        while (true) {
            for (int i = 0; i < 10; i++) {
                vector.add(i);
            }

            Thread removeThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < vector.size(); i++) {
                        vector.remove(i);
                    }
                }
            });

            Thread printThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < vector.size(); i++) {
                        System.out.println((vector.get(i)));
                    }
                }
            });

            removeThread.start();
            printThread.start();

            //不要同時產生過多的執行緒,否則會導致作業系統假死
            while (Thread.activeCount() > 20);
        }
    }

執行結果如下:

Exception in thread"Thread-132"java.lang.ArrayIndexOutOfBoundsException:
Array index out of range:17
at java.util.Vector.remove(Vector.java777)
at org.fenixsoft.mulithread.VectorTest$1.run(VectorTest.java21)
at java.lang.Thread.run(Thread.java662

很明顯,儘管這裡使用到的Vector的get()、remove()和size()方法都是同步的,但是在多執行緒的環境中,如果不在方法呼叫端做額外的同步措施的話,使用這段程式碼仍然是不安全的,因為如果另一個執行緒恰好在錯誤的時間裡刪除了一個元素,導致序號i已經不再可用的話,再用i訪問陣列就會丟擲一個ArrayIndexOutOfBoundsException。如果要保證這段程式碼能正確執行下去,我們不得不把removeThread和printThread的定義改成如程式碼清單13-3所示的樣子。

程式碼清單13-3 必須加入同步以保證Vector訪問的執行緒安全性

 Thread removeThread = new Thread(new Runnable() {
        @Override
        public void run() {
            synchronized (vector) {
                for (int i = 0; i < vector.size(); i++) {
                    vector.remove(i);
                }
            }
        }
    });

    Thread printThread = new Thread(new Runnable() {
        @Override
        public void run() {
            synchronized (vector) {
                for (int i = 0; i < vector.size(); i++) {
                    System.out.println((vector.get(i)));
                }
            }
        }
    });

3.相對執行緒安全

相對的執行緒安全就是我們通常意義上所講的執行緒安全,它需要保證對這個物件單獨的操作是執行緒安全的,我們在呼叫的時候不需要做額外的保障措施,但是對於一些特定順序的連續呼叫,就可能需要在呼叫端使用額外的同步手段來保證呼叫的正確性。上面程式碼清單13-2和程式碼清單13-3就是相對執行緒安全的明顯的案例。

在Java語言中,大部分的執行緒安全類都屬於這種型別,例如Vector、HashTable、Collections的synchronizedCollection()方法包裝的集合等。

4.執行緒相容

執行緒相容是指物件本身並不是執行緒安全的,但是可以通過在呼叫端正確地使用同步手段來保證物件在併發環境中可以安全地使用,我們平常說一個類不是執行緒安全的,絕大多數時候指的是這一種情況。Java API中大部分的類都是屬於執行緒相容的,如與前面的Vector和HashTable相對應的集合類ArrayList和HashMap等。

5.執行緒對立

執行緒對立是指無論呼叫端是否採取了同步措施,都無法在多執行緒環境中併發使用的程式碼。由於Java語言天生就具備多執行緒特性,執行緒對立這種排斥多執行緒的程式碼是很少出現的,而且通常都是有害的,應當儘量避免。

一個執行緒對立的例子是Thread類的suspend()和resume()方法,如果有兩個執行緒同時持有一個執行緒物件,一個嘗試去中斷執行緒,另一個嘗試去恢復執行緒,如果併發進行的話,無論呼叫時是否進行了同步,目標執行緒都是存在死鎖風險的,如果suspend()中斷的執行緒就是即將要執行resume()的那個執行緒,那就肯定要產生死鎖了。也正是由於這個原因,suspend()和resume()方法已經被JDK宣告廢棄(@Deprecated)了。常見的執行緒對立的操作還有System.setIn()、Sytem.setOut()和System.runFinalizersOnExit()等。

執行緒安全的實現方法

瞭解了什麼是執行緒安全之後,緊接著的一個問題就是我們應該如何實現執行緒安全,這聽起來似乎是一件由程式碼如何編寫來決定的事情,確實,如何實現執行緒安全與程式碼編寫有很大的關係,但虛擬機器提供的同步和鎖機制也起到了非常重要的作用。本節中,程式碼編寫如何實現執行緒安全和虛擬機器如何實現同步與鎖這兩者都會有所涉及,相對而言更偏重後者一些,只要讀者瞭解了虛擬機器執行緒安全手段的運作過程,自己去思考程式碼如何編寫並不是一件困難的事情。

1.互斥同步

互斥同步(Mutual Exclusion&Synchronization)是常見的一種併發正確性保障手段。同步是指在多個執行緒併發訪問共享資料時,保證共享資料在同一個時刻只被一個(或者是一些,使用訊號量的時候)執行緒使用。而互斥是實現同步的一種手段,臨界區(Critical Section)、互斥量(Mutex)和訊號量(Semaphore)都是主要的互斥實現方式。因此,在這4個字裡面,互斥是因,同步是果;互斥是方法,同步是目的。

在Java中,最基本的互斥同步手段就是synchronized關鍵字,synchronized關鍵字經過編譯之後,會在同步塊的前後分別形成monitorenter和monitorexit這兩個位元組碼指令,這兩個位元組碼都需要一個reference型別的引數來指明要鎖定和解鎖的物件。如果Java程式中的synchronized明確指定了物件引數,那就是這個物件的reference;如果沒有明確指定,那就根據synchronized修飾的是例項方法還是類方法,去取對應的物件例項或Class物件來作為鎖物件。

根據虛擬機器規範的要求,在執行monitorenter指令時,首先要嘗試獲取物件的鎖。如果這個物件沒被鎖定,或者當前執行緒已經擁有了那個物件的鎖,把鎖的計數器加1,相應的,在執行monitorexit指令時會將鎖計數器減1,當計數器為0時,鎖就被釋放。如果獲取物件鎖失敗,那當前執行緒就要阻塞等待,直到物件鎖被另外一個執行緒釋放為止。

在虛擬機器規範對monitorenter和monitorexit的行為描述中,有兩點是需要特別注意的。首先,synchronized同步塊對同一條執行緒來說是可重入的,不會出現自己把自己鎖死的問題。其次,同步塊在已進入的執行緒執行完之前,會阻塞後面其他執行緒的進入。Java的執行緒是對映到作業系統的原生執行緒之上的,如果要阻塞或喚醒一個執行緒,都需要作業系統來幫忙完成,這就需要從使用者態轉換到核心態中,因此狀態轉換需要耗費很多的處理器時間。對於程式碼簡單的同步塊(如被synchronized修飾的getter()或setter()方法),狀態轉換消耗的時間有可能比使用者程式碼執行的時間還要長。所以synchronized是Java語言中一個重量級(Heavyweight)的操作,有經驗的程式設計師都會在確實必要的情況下才使用這種操作。而虛擬機器本身也會進行一些優化,譬如在通知作業系統阻塞執行緒之前加入一段自旋等待過程,避免頻繁地切入到核心態之中。

除了synchronized之外,我們還可以使用java.util.concurrent(下文稱J.U.C)包中的重入鎖(ReentrantLock)來實現同步,在基本用法上,ReentrantLock與synchronized很相似,他們都具備一樣的執行緒重入特性,只是程式碼寫法上有點區別,一個表現為API層面的互斥鎖(lock()和unlock()方法配合try/finally語句塊來完成),另一個表現為原生語法層面的互斥鎖。不過,相比synchronized,ReentrantLock增加了一些高階功能,主要有以下3項:等待可中斷、可實現公平鎖,以及鎖可以繫結多個條件。

等待可中斷是指當持有鎖的執行緒長期不釋放鎖的時候,正在等待的執行緒可以選擇放棄等待,改為處理其他事情,可中斷特性對處理執行時間非常長的同步塊很有幫助。

公平鎖是指多個執行緒在等待同一個鎖時,必須按照申請鎖的時間順序來依次獲得鎖;而非公平鎖則不保證這一點,在鎖被釋放時,任何一個等待鎖的執行緒都有機會獲得鎖。synchronized中的鎖是非公平的,ReentrantLock預設情況下也是非公平的,但可以通過帶布林值的建構函式要求使用公平鎖。

鎖繫結多個條件是指一個ReentrantLock物件可以同時繫結多個Condition物件,而在synchronized中,鎖物件的wait()和notify()或notifyAll()方法可以實現一個隱含的條件,如果要和多於一個的條件關聯的時候,就不得不額外地新增一個鎖,而ReentrantLock則無須這樣做,只需要多次呼叫newCondition()方法即可。

如果需要使用上述功能,選用ReentrantLock是一個很好的選擇,那如果是基於效能考慮呢?關於synchronized和ReentrantLock的效能問題,Brian Goetz對這兩種鎖在JDK 1.5與單核處理器,以及JDK 1.5與雙Xeon處理器環境下做了一組吞吐量對比的實驗,實驗結果如圖13-1和圖13-2所示。

圖 13-1 JDK 1.5、單核處理器下兩種鎖的吞吐量對比

圖 13-2 JDK 1.5、雙Xeon處理器下兩種鎖的吞吐量對比

從圖13-1和圖13-2可以看出,多執行緒環境下synchronized的吞吐量下降得非常嚴重,而ReentrantLock則能基本保持在同一個比較穩定的水平上。與其說ReentrantLock效能好,還不如說synchronized還有非常大的優化餘地。後續的技術發展也證明了這一點,JDK 1.6中加入了很多針對鎖的優化措施(13.3節我們就會講解這些優化措施),JDK 1.6釋出之後,人們就發現synchronized與ReentrantLock的效能基本上是完全持平了。因此,如果讀者的程式是使用JDK 1.6或以上部署的話,效能因素就不再是選擇ReentrantLock的理由了,虛擬機器在未來的效能改進中肯定也會更加偏向於原生的synchronized,所以還是提倡在synchronized能實現需求的情況下,優先考慮使用synchronized來進行同步。

2.非阻塞同步

互斥同步最主要的問題就是進行執行緒阻塞和喚醒所帶來的效能問題,因此這種同步也稱為阻塞同步(Blocking Synchronization)。從處理問題的方式上說,互斥同步屬於一種悲觀的併發策略,總是認為只要不去做正確的同步措施(例如加鎖),那就肯定會出現問題,無論共享資料是否真的會出現競爭,它都要進行加鎖(這裡討論的是概念模型,實際上虛擬機器會優化掉很大一部分不必要的加鎖)、使用者態核心態轉換、維護鎖計數器和檢查是否有被阻塞的執行緒需要喚醒等操作。隨著硬體指令集的發展,我們有了另外一個選擇:基於衝突檢測的樂觀併發策略,通俗地說,就是先進行操作,如果沒有其他執行緒爭用共享資料,那操作就成功了;如果共享資料有爭用,產生了衝突,那就再採取其他的補償措施(最常見的補償措施就是不斷地重試,直到成功為止),這種樂觀的併發策略的許多實現都不需要把執行緒掛起,因此這種同步操作稱為非阻塞同步(Non-Blocking Synchronization)。

為什麼筆者說使用樂觀併發策略需要“硬體指令集的發展”才能進行呢?因為我們需要操作和衝突檢測這兩個步驟具備原子性,靠什麼來保證呢?如果這裡再使用互斥同步來保證就失去意義了,所以我們只能靠硬體來完成這件事情,硬體保證一個從語義上看起來需要多次操作的行為只通過一條處理器指令就能完成,這類指令常用的有:

  • 測試並設定(Test-and-Set)。
  • 獲取並增加(Fetch-and-Increment)。
  • 交換(Swap)。
  • 比較並交換(Compare-and-Swap,下文稱CAS)。
  • 載入連結/條件儲存(Load-Linked/Store-Conditional,下文稱LL/SC)。

其中,前面的3條是20世紀就已經存在於大多數指令集之中的處理器指令,後面的兩條是現代處理器新增的,而且這兩條指令的目的和功能是類似的。在IA64、x86指令集中有cmpxchg指令完成CAS功能,在sparc-TSO也有casa指令實現,而在ARM和PowerPC架構下,則需要使用一對ldrex/strex指令來完成LL/SC的功能。

CAS指令需要有3個運算元,分別是記憶體位置(在Java中可以簡單理解為變數的記憶體地址,用V表示)、舊的預期值(用A表示)和新值(用B表示)。CAS指令執行時,當且僅當V符合舊預期值A時,處理器用新值B更新V的值,否則它就不執行更新,但是無論是否更新了V的值,都會返回V的舊值,上述的處理過程是一個原子操作。

在JDK 1.5之後,Java程式中才可以使用CAS操作,該操作由sun.misc.Unsafe類裡面的compareAndSwapInt()和compareAndSwapLong()等幾個方法包裝提供,虛擬機器在內部對這些方法做了特殊處理,即時編譯出來的結果就是一條平臺相關的處理器CAS指令,沒有方法呼叫的過程,或者可以認為是無條件內聯進去了。

由於Unsafe類不是提供給使用者程式呼叫的類(Unsafe.getUnsafe()的程式碼中限制了只有啟動類載入器(Bootstrap ClassLoader)載入的Class才能訪問它),因此,如果不採用反射手段,我們只能通過其他的Java API來間接使用它,如J.U.C包裡面的整數原子類,其中的compareAndSet()和getAndIncrement()等方法都使用了Unsafe類的CAS操作。

我們不妨拿一段在第12章中沒有解決的問題程式碼來看看如何使用CAS操作來避免阻塞同步,程式碼如程式碼清單12-1所示。我們曾經通過這段20個執行緒自增10000次的程式碼來證明volatile變數不具備原子性,那麼如何才能讓它具備原子性呢?把“race++”操作或increase()方法用同步塊包裹起來當然是一個辦法,但是如果改成如程式碼清單13-4所示的程式碼,那效率將會提高許多。

程式碼清單13-4 Atomic的原子自增運算

/**
 * Atomic變數自增運算測試
 * 
 * @author zzm
*/
public class AtomicTest {

    public static AtomicInteger race = new AtomicInteger(0);

    public static void increase() {
        race.incrementAndGet();
    }

    private static final int THREADS_COUNT = 20;

    public static void main(String[] args) throws Exception {
        Thread[] threads = new Thread[THREADS_COUNT];
        for (int i = 0; i < THREADS_COUNT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        increase();
                    }
                }
            });
            threads[i].start();
        }

        while (Thread.activeCount() > 1)
            Thread.yield();

        System.out.println(race);
    }
}

執行結果如下:

200000

使用AtomicInteger代替int後,程式輸出了正確的結果,一切都要歸功於incrementAndGet()方法的原子性。它的實現其實非常簡單,如程式碼清單13-5所示。

程式碼清單13-5 incrementAndGet()方法的JDK原始碼

    /**
     * Atomically increment by one the current value.
     * @return the updated value
     */
    public final int incrementAndGet() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return next;
        }
    }

incrementAndGet()方法在一個無限迴圈中,不斷嘗試將一個比當前值大1的新值賦給自己。如果失敗了,那說明在執行“獲取-設定”操作的時候值已經有了修改,於是再次迴圈進行下一次操作,直到設定成功為止。

儘管CAS看起來很美,但顯然這種操作無法涵蓋互斥同步的所有使用場景,並且CAS從語義上來說並不是完美的,存在這樣的一個邏輯漏洞:如果一個變數V初次讀取的時候是A值,並且在準備賦值的時候檢查到它仍然為A值,那我們就能說它的值沒有被其他執行緒改變過了嗎?如果在這段期間它的值曾經被改成了B,後來又被改回為A,那CAS操作就會誤認為它從來沒有被改變過。這個漏洞稱為CAS操作的“ABA”問題。J.U.C包為了解決這個問題,提供了一個帶有標記的原子引用類“AtomicStampedReference”,它可以通過控制變數值的版本來保證CAS的正確性。不過目前來說這個類比較“雞肋”,大部分情況下ABA問題不會影響程式併發的正確性,如果需要解決ABA問題,改用傳統的互斥同步可能會比原子類更高效。

3.無同步方案

要保證執行緒安全,並不是一定就要進行同步,兩者沒有因果關係。同步只是保證共享資料爭用時的正確性的手段,如果一個方法本來就不涉及共享資料,那它自然就無須任何同步措施去保證正確性,因此會有一些程式碼天生就是執行緒安全的,筆者簡單地介紹其中的兩類。

可重入程式碼(Reentrant Code):這種程式碼也叫做純程式碼(Pure Code),可以在程式碼執行的任何時刻中斷它,轉而去執行另外一段程式碼(包括遞迴呼叫它本身),而在控制權返回後,原來的程式不會出現任何錯誤。相對執行緒安全來說,可重入性是更基本的特性,它可以保證執行緒安全,即所有的可重入的程式碼都是執行緒安全的,但是並非所有的執行緒安全的程式碼都是可重入的。

可重入程式碼有一些共同的特徵,例如不依賴儲存在堆上的資料和公用的系統資源、用到的狀態量都由引數中傳入、不呼叫非可重入的方法等。我們可以通過一個簡單的原則來判斷程式碼是否具備可重入性:如果一個方法,它的返回結果是可以預測的,只要輸入了相同的資料,就都能返回相同的結果,那它就滿足可重入性的要求,當然也就是執行緒安全的。

執行緒本地儲存(Thread Local Storage):如果一段程式碼中所需要的資料必須與其他程式碼共享,那就看看這些共享資料的程式碼是否能保證在同一個執行緒中執行?如果能保證,我們就可以把共享資料的可見範圍限制在同一個執行緒之內,這樣,無須同步也能保證執行緒之間不出現資料爭用的問題。

符合這種特點的應用並不少見,大部分使用消費佇列的架構模式(如“生產者-消費者”模式)都會將產品的消費過程儘量在一個執行緒中消費完,其中最重要的一個應用例項就是經典Web互動模型中的“一個請求對應一個伺服器執行緒”(Thread-per-Request)的處理方式,這種處理方式的廣泛應用使得很多Web服務端應用都可以使用執行緒本地儲存來解決執行緒安全問題。

Java語言中,如果一個變數要被多執行緒訪問,可以使用volatile關鍵字宣告它為“易變的”;如果一個變數要被某個執行緒獨享,Java中就沒有類似C++中__declspec(thread)這樣的關鍵字,不過還是可以通過java.lang.ThreadLocal類來實現執行緒本地儲存的功能。每一個執行緒的Thread物件中都有一個ThreadLocalMap物件,這個物件儲存了一組以ThreadLocal.threadLocalHashCode為鍵,以本地執行緒變數為值的K-V值對,ThreadLocal物件就是當前執行緒的ThreadLocalMap的訪問入口,每一個ThreadLocal物件都包含了一個獨一無二的threadLocalHashCode值,使用這個值就可以線上程K-V值對中找回對應的本地執行緒變數。

相關文章