當前位置:才華齋>計算機>java語言>

簡單地分析Java執行緒程式設計中ThreadLocal類的使用

java語言 閱讀(1.66W)

  一、概述

簡單地分析Java執行緒程式設計中ThreadLocal類的使用

ThreadLocal是什麼呢?其實ThreadLocal並非是一個執行緒的本地實現版本,它並不是一個Thread,而是threadlocalvariable(執行緒區域性變數)。也許把它命名為ThreadLocalVar更加合適。執行緒區域性變數(ThreadLocal)其實的功用非常簡單,就是為每一個使用該變數的執行緒都提供一個變數值的副本,是Java中一種較為特殊的執行緒繫結機制,是每一個執行緒都可以獨立地改變自己的副本,而不會和其它執行緒的副本衝突。

從執行緒的角度看,每個執行緒都保持一個對其執行緒區域性變數副本的隱式引用,只要執行緒是活動的並且 ThreadLocal 例項是可訪問的;線上程消失之後,其執行緒區域性例項的所有副本都會被垃圾回收(除非存在對這些副本的其他引用)。

通過ThreadLocal存取的資料,總是與當前執行緒相關,也就是說,JVM 為每個執行的執行緒,綁定了私有的本地例項存取空間,從而為多執行緒環境常出現的併發訪問問題提供了一種隔離機制。

ThreadLocal是如何做到為每一個執行緒維護變數的副本的呢?其實實現的思路很簡單,在ThreadLocal類中有一個Map,用於儲存每一個執行緒的變數的副本。

概括起來說,對於多執行緒資源共享的問題,同步機制採用了“以時間換空間”的方式,而ThreadLocal採用了“以空間換時間”的`方式。前者僅提供一份變數,讓不同的執行緒排隊訪問,而後者為每一個執行緒都提供了一份變數,因此可以同時訪問而互不影響。

  二、API說明

ThreadLocal()

建立一個執行緒本地變數。

T get()

返回此執行緒區域性變數的當前執行緒副本中的值,如果這是執行緒第一次呼叫該方法,則建立並初始化此副本。

protected T initialValue()

返回此執行緒區域性變數的當前執行緒的初始值。最多在每次訪問執行緒來獲得每個執行緒區域性變數時呼叫此方法一次,即執行緒第一次使用 get() 方法訪問變數的時候。如果執行緒先於 get 方法呼叫 set(T) 方法,則不會線上程中再呼叫 initialValue 方法。

若該實現只返回 null;如果程式設計師希望將執行緒區域性變數初始化為 null 以外的某個值,則必須為 ThreadLocal 建立子類,並重寫此方法。通常,將使用匿名內部類。initialValue 的典型實現將呼叫一個適當的構造方法,並返回新構造的物件。

void remove()

移除此執行緒區域性變數的值。這可能有助於減少執行緒區域性變數的儲存需求。如果再次訪問此執行緒區域性變數,那麼在預設情況下它將擁有其 initialValue。

void set(T value)

將此執行緒區域性變數的當前執行緒副本中的值設定為指定值。許多應用程式不需要這項功能,它們只依賴於 initialValue() 方法來設定執行緒區域性變數的值。

在程式中一般都重寫initialValue方法,以給定一個特定的初始值。

  三、一.對ThreadLocal的理解

ThreadLocal,很多地方叫做執行緒本地變數,也有些地方叫做執行緒本地儲存,其實意思差不多。可能很多朋友都知道ThreadLocal為變數在每個執行緒中都建立了一個副本,那麼每個執行緒可以訪問自己內部的副本變數。

這句話從字面上看起來很容易理解,但是真正理解並不是那麼容易。

我們還是先來看一個例子:

class ConnectionManager { private static Connection connect = null; public static Connection openConnection() { if(connect == null){ connect = onnection(); } return connect; } public static void closeConnection() { if(connect!=null) e(); }}

假設有這樣一個數據庫連結管理類,這段程式碼在單執行緒中使用是沒有任何問題的,但是如果在多執行緒中使用呢?很顯然,在多執行緒中使用會存線上程安全問題:第一,這裡面的2個方法都沒有進行同步,很可能在openConnection方法中會多次建立connect;第二,由於connect是共享變數,那麼必然在呼叫connect的地方需要使用到同步來保障執行緒安全,因為很可能一個執行緒在使用connect進行資料庫操作,而另外一個執行緒呼叫closeConnection關閉連結。

所以出於執行緒安全的考慮,必須將這段程式碼的兩個方法進行同步處理,並且在呼叫connect的地方需要進行同步處理。

這樣將會大大影響程式執行效率,因為一個執行緒在使用connect進行資料庫操作的時候,其他執行緒只有等待。

那麼大家來仔細分析一下這個問題,這地方到底需不需要將connect變數進行共享?事實上,是不需要的。假如每個執行緒中都有一個connect變數,各個執行緒之間對connect變數的訪問實際上是沒有依賴關係的,即一個執行緒不需要關心其他執行緒是否對這個connect進行了修改的。

到這裡,可能會有朋友想到,既然不需要線上程之間共享這個變數,可以直接這樣處理,在每個需要使用資料庫連線的方法中具體使用時才建立資料庫連結,然後在方法呼叫完畢再釋放這個連線。比如下面這樣:

class ConnectionManager { private Connection connect = null; public Connection openConnection() { if(connect == null){ connect = onnection(); } return connect; } public void closeConnection() { if(connect!=null) e(); }} class Dao{ public void () { ConnectionManager connectionManager = new ConnectionManager(); Connection connection = Connection(); //使用connection進行操作 eConnection(); }}

這樣處理確實也沒有任何問題,由於每次都是在方法內部建立的連線,那麼執行緒之間自然不存線上程安全問題。但是這樣會有一個致命的影響:導致伺服器壓力非常大,並且嚴重影響程式執行效能。由於在方法中需要頻繁地開啟和關閉資料庫連線,這樣不盡嚴重影響程式執行效率,還可能導致伺服器壓力巨大。

那麼這種情況下使用ThreadLocal是再適合不過的了,因為ThreadLocal在每個執行緒中對該變數會建立一個副本,即每個執行緒內部都會有一個該變數,且線上程內部任何地方都可以使用,執行緒之間互不影響,這樣一來就不存線上程安全問題,也不會嚴重影響程式執行效能。

但是要注意,雖然ThreadLocal能夠解決上面說的問題,但是由於在每個執行緒中都建立了副本,所以要考慮它對資源的消耗,比如記憶體的佔用會比不使用ThreadLocal要大。

  四、例項

建立一個Bean,通過不同的執行緒物件設定Bean屬性,保證各個執行緒Bean物件的獨立性。

/** * Created by IntelliJ IDEA. * User: leizhimin * Date: 2007-11-23 * Time: 10:45:02 * 學生 */public class Student { private int age = 0; //年齡 public int getAge() { return ; } public void setAge(int age) { = age; }} /** * Created by IntelliJ IDEA. * User: leizhimin * Date: 2007-11-23 * Time: 10:53:33 * 多執行緒下測試程式 */public class ThreadLocalDemo implements Runnable { //建立執行緒區域性變數studentLocal,在後面你會發現用來儲存Student物件 private final static ThreadLocal studentLocal = new ThreadLocal(); public static void main(String[] agrs) { ThreadLocalDemo td = new ThreadLocalDemo(); Thread t1 = new Thread(td, "a"); Thread t2 = new Thread(td, "b"); t(); t(); } public void run() { accessStudent(); } /** * 示例業務方法,用來測試 */ public void accessStudent() { //獲取當前執行緒的名字 String currentThreadName = entThread()ame(); tln(currentThreadName + " is running!"); //產生一個隨機數並列印 Random random = new Random(); int age = Int(100); tln("thread " + currentThreadName + " set age to:" + age); //獲取一個Student物件,並將隨機數年齡插入到物件屬性中 Student student = getStudent(); ge(age); tln("thread " + currentThreadName + " first read age is:" + ge()); try { p(500); } catch (InterruptedException ex) { tStackTrace(); } tln("thread " + currentThreadName + " second read age is:" + ge()); } protected Student getStudent() { //獲取本地執行緒變數並強制轉換為Student型別 Student student = (Student) (); //執行緒首次執行此方法的時候,()肯定為null if (student == null) { //建立一個Student物件,並儲存到本地執行緒變數studentLocal中 student = new Student(); (student); } return student; }}

執行結果:

a is running! thread a set age to:76 b is running! thread b set age to:27 thread a first read age is:76 thread b first read age is:27 thread a second read age is:76 thread b second read age is:27

可以看到a、b兩個執行緒age在不同時刻列印的值是完全相同的。這個程式通過妙用ThreadLocal,既實現多執行緒併發,遊兼顧資料的安全性。

 五、ThreadLocal使用的一般步驟

1、在多執行緒的類(如ThreadDemo類)中,建立一個ThreadLocal物件threadXxx,用來儲存執行緒間需要隔離處理的物件xxx。

2、在ThreadDemo類中,建立一個獲取要隔離訪問的資料的方法getXxx(),在方法中判斷,若ThreadLocal物件為null時候,應該new()一個隔離訪問型別的物件,並強制轉換為要應用的型別。

3、在ThreadDemo類的run()方法中,通過getXxx()方法獲取要操作的資料,這樣可以保證每個執行緒對應一個數據物件,在任何時刻都操作的是這個物件。