【轉】Java併發編程:深刻剖析ThreadLocal

來自: http://www.importnew.com/17849.htmlhtml

想必不少朋友對ThreadLocal並不陌生,今天咱們就來一塊兒探討下ThreadLocal的使用方法和實現原理。首先,本文先談一下對 ThreadLocal的理解,而後根據ThreadLocal類的源碼分析了其實現原理和使用須要注意的地方,最後給出了兩個應用場景。java

如下是本文目錄大綱:spring

一.對ThreadLocal的理解數據庫

二.深刻解析ThreadLocal類編程

三.ThreadLocal的應用場景安全

如有不正之處請多多諒解,並歡迎批評指正。服務器

一.對ThreadLocal的理解

ThreadLocal,不少地方叫作線程本地變量,也有些地方叫作線程本地存儲,其實意思差很少。可能不少朋友都知道ThreadLocal爲變量在每一個線程中都建立了一個副本,那麼每一個線程能夠訪問本身內部的副本變量。多線程

這句話從字面上看起來很容易理解,可是真正理解並非那麼容易。源碼分析

咱們仍是先來看一個例子:性能

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ConnectionManager {
 
  private static Connection connect = null ;
 
  public static Connection openConnection() {
  if (connect == null ){
  connect = DriverManager.getConnection();
  }
  return connect;
  }
 
  public static void closeConnection() {
  if (connect!= null )
  connect.close();
  }
}

假設有這樣一個數據庫連接管理類,這段代碼在單線程中使用是沒有任何問題的,可是若是在多線程中使用呢?很顯然,在多線程中使用會存在線程安全問 題:第一,這裏面的2個方法都沒有進行同步,極可能在openConnection方法中會屢次建立connect;第二,因爲connect是共享變 量,那麼必然在調用connect的地方須要使用到同步來保障線程安全,由於極可能一個線程在使用connect進行數據庫操做,而另一個線程調用 closeConnection關閉連接。

因此出於線程安全的考慮,必須將這段代碼的兩個方法進行同步處理,而且在調用connect的地方須要進行同步處理。

這樣將會大大影響程序執行效率,由於一個線程在使用connect進行數據庫操做的時候,其餘線程只有等待。

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

到這裏,可能會有朋友想到,既然不須要在線程之間共享這個變量,能夠直接這樣處理,在每一個須要使用數據庫鏈接的方法中具體使用時才建立數據庫連接,而後在方法調用完畢再釋放這個鏈接。好比下面這樣:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class ConnectionManager {
 
  private Connection connect = null ;
 
  public Connection openConnection() {
  if (connect == null ){
  connect = DriverManager.getConnection();
  }
  return connect;
  }
 
  public void closeConnection() {
  if (connect!= null )
  connect.close();
  }
}
 
class Dao{
  public void insert() {
  ConnectionManager connectionManager = new ConnectionManager();
  Connection connection = connectionManager.openConnection();
 
  //使用connection進行操做
 
  connectionManager.closeConnection();
  }
}

這樣處理確實也沒有任何問題,因爲每次都是在方法內部建立的鏈接,那麼線程之間天然不存在線程安全問題。可是這樣會有一個致命的影響:致使服務器 壓力很是大,而且嚴重影響程序執行性能。因爲在方法中須要頻繁地開啓和關閉數據庫鏈接,這樣不盡嚴重影響程序執行效率,還可能致使服務器壓力巨大。

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

可是要注意,雖然ThreadLocal可以解決上面說的問題,可是因爲在每一個線程中都建立了副本,因此要考慮它對資源的消耗,好比內存的佔用會比不使用ThreadLocal要大。

二.深刻解析ThreadLocal類

在上面談到了對ThreadLocal的一些理解,那咱們下面來看一下具體ThreadLocal是如何實現的。

先了解一下ThreadLocal類提供的幾個方法:

?
1
2
3
4
public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }

get()方法是用來獲取ThreadLocal在當前線程中保存的變量副本,set()用來設置當前線程中變量的副本,remove()用來移 除當前線程中變量的副本,initialValue()是一個protected方法,通常是用來在使用時進行重寫的,它是一個延遲加載方法,下面會詳細 說明。

首先咱們來看一下ThreadLocal類是如何爲每一個線程建立一個變量的副本的。

先看下get方法的實現:

第一句是取得當前線程,而後經過getMap(t)方法獲取到一個map,map的類型爲ThreadLocalMap。而後接着下面獲取到<key,value>鍵值對,注意這裏獲取鍵值對傳進去的是 this,而不是當前線程t。

若是獲取成功,則返回value值。

若是map爲空,則調用setInitialValue方法返回value。

咱們上面的每一句來仔細分析:

首先看一下getMap方法中作了什麼:

可能你們沒有想到的是,在getMap中,是調用當期線程t,返回當前線程t中的一個成員變量threadLocals。

那麼咱們繼續取Thread類中取看一下成員變量threadLocals是什麼:

實際上就是一個ThreadLocalMap,這個類型是ThreadLocal類的一個內部類,咱們繼續取看ThreadLocalMap的實現:

能夠看到ThreadLocalMap的Entry繼承了WeakReference,而且使用ThreadLocal做爲鍵值。

而後再繼續看setInitialValue方法的具體實現:

很容易瞭解,就是若是map不爲空,就設置鍵值對,爲空,再建立Map,看一下createMap的實現:

至此,可能大部分朋友已經明白了ThreadLocal是如何爲每一個線程建立變量的副本的:

首先,在每一個線程Thread內部有一個ThreadLocal.ThreadLocalMap類型的成員變量threadLocals,這個 threadLocals就是用來存儲實際的變量副本的,鍵值爲當前ThreadLocal變量,value爲變量副本(即T類型的變量)。

初始時,在Thread裏面,threadLocals爲空,當經過ThreadLocal變量調用get()方法或者set()方法,就會對 Thread類中的threadLocals進行初始化,而且以當前ThreadLocal變量爲鍵值,以ThreadLocal要保存的副本變量爲 value,存到threadLocals。

而後在當前線程裏面,若是要使用副本變量,就能夠經過get方法在threadLocals裏面查找。

下面經過一個例子來證實經過ThreadLocal能達到在每一個線程中建立變量副本的效果:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class Test {
  ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
  ThreadLocal<String> stringLocal = new ThreadLocal<String>();
 
  public void set() {
  longLocal.set(Thread.currentThread().getId());
  stringLocal.set(Thread.currentThread().getName());
  }
 
  public long getLong() {
  return longLocal.get();
  }
 
  public String getString() {
  return stringLocal.get();
  }
 
  public static void main(String[] args) throws InterruptedException {
  final Test test = new Test();
 
  test.set();
  System.out.println(test.getLong());
  System.out.println(test.getString());
 
  Thread thread1 = new Thread(){
  public void run() {
  test.set();
  System.out.println(test.getLong());
  System.out.println(test.getString());
  };
  };
  thread1.start();
  thread1.join();
 
  System.out.println(test.getLong());
  System.out.println(test.getString());
  }
}

這段代碼的輸出結果爲:

從這段代碼的輸出結果能夠看出,在main線程中和thread1線程中,longLocal保存的副本值和stringLocal保存的副本值都不同。最後一次在main線程再次打印副本值是爲了證實在main線程中和thread1線程中的副本值確實是不一樣的。

總結一下:

1)實際的經過ThreadLocal建立的副本是存儲在每一個線程本身的threadLocals中的;

2)爲什麼threadLocals的類型ThreadLocalMap的鍵值爲ThreadLocal對象,由於每一個線程中可有多個threadLocal變量,就像上面代碼中的longLocal和stringLocal;

3)在進行get以前,必須先set,不然會報空指針異常;

若是想在get以前不須要調用set就能正常訪問的話,必須重寫initialValue()方法。

由於在上面的代碼分析過程當中,咱們發現若是沒有先set的話,即在map中查找不到對應的存儲,則會經過調用setInitialValue方法 返回i,而在setInitialValue方法中,有一個語句是T value = initialValue(), 而默認狀況下,initialValue方法返回的是null。

看下面這個例子:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Test {
  ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
  ThreadLocal<String> stringLocal = new ThreadLocal<String>();
 
  public void set() {
  longLocal.set(Thread.currentThread().getId());
  stringLocal.set(Thread.currentThread().getName());
  }
 
  public long getLong() {
  return longLocal.get();
  }
 
  public String getString() {
  return stringLocal.get();
  }
 
  public static void main(String[] args) throws InterruptedException {
  final Test test = new Test();
 
  System.out.println(test.getLong());
  System.out.println(test.getString());
 
  Thread thread1 = new Thread(){
  public void run() {
  test.set();
  System.out.println(test.getLong());
  System.out.println(test.getString());
  };
  };
  thread1.start();
  thread1.join();
 
  System.out.println(test.getLong());
  System.out.println(test.getString());
  }
}

在main線程中,沒有先set,直接get的話,運行時會報空指針異常。

可是若是改爲下面這段代碼,即重寫了initialValue方法:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class Test {
  ThreadLocal<Long> longLocal = new ThreadLocal<Long>(){
  protected Long initialValue() {
  return Thread.currentThread().getId();
  };
  };
  ThreadLocal<String> stringLocal = new ThreadLocal<String>(){;
  protected String initialValue() {
  return Thread.currentThread().getName();
  };
  };
 
  public void set() {
  longLocal.set(Thread.currentThread().getId());
  stringLocal.set(Thread.currentThread().getName());
  }
 
  public long getLong() {
  return longLocal.get();
  }
 
  public String getString() {
  return stringLocal.get();
  }
 
  public static void main(String[] args) throws InterruptedException {
  final Test test = new Test();
 
  test.set();
  System.out.println(test.getLong());
  System.out.println(test.getString());
 
  Thread thread1 = new Thread(){
  public void run() {
  test.set();
  System.out.println(test.getLong());
  System.out.println(test.getString());
  };
  };
  thread1.start();
  thread1.join();
 
  System.out.println(test.getLong());
  System.out.println(test.getString());
  }
}

就能夠直接不用先set而直接調用get了。

三.ThreadLocal的應用場景

最多見的ThreadLocal使用場景爲 用來解決 數據庫鏈接、Session管理等。

如:

?
1
2
3
4
5
6
7
8
9
10
private static ThreadLocal<Connection> connectionHolder
= new ThreadLocal<Connection>() {
public Connection initialValue() {
  return DriverManager.getConnection(DB_URL);
}
};
 
public static Connection getConnection() {
return connectionHolder.get();
}

下面這段代碼摘自:

http://www.iteye.com/topic/103804

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static final ThreadLocal threadSession = new ThreadLocal();
 
public static Session getSession() throws InfrastructureException {
  Session s = (Session) threadSession.get();
  try {
  if (s == null ) {
  s = getSessionFactory().openSession();
  threadSession.set(s);
  }
  } catch (HibernateException ex) {
  throw new InfrastructureException(ex);
  }
  return s;
}

參考資料:

《深刻理解Java虛擬機》

《 Java編程思想 》

http://ifeve.com/thread-management-10/

http://www.ibm.com/developerworks/cn/java/j-threads/index3.html

http://www.iteye.com/topic/103804

http://www.iteye.com/topic/777716

http://www.iteye.com/topic/757478

http://blog.csdn.net/ghsau/article/details/15732053

http://ispring.iteye.com/blog/162982

http://blog.csdn.net/imzoer/article/details/8262101

http://www.blogjava.net/wumi9527/archive/2010/09/10/331654.html

http://bbs.csdn.net/topics/380049261

相關文章
相關標籤/搜索