Java多線程——線程封閉


  線程封閉:當訪問共享的可變數據時,通常需要同步。一種避免同步的方式就是不共享數據。如果僅在單線程內訪問數據,就不需要同步,這種技術稱為線程封閉(thread  confinement)

  線程封閉技術一個常見的應用就是JDBC的Connection對象,JDBC規范並沒有要求Connection對象必須是線程安全的,在服務器應用程序中,線程從連接池獲取一個Connection對象,使用完之后將對象返還給連接池。下面介紹幾種線程封閉技術:

  1、Ad-hoc線程封閉

  Ad-hoc線程封閉是指,維護線程的封閉性的職責完全由程序實現承擔,是非常脆弱的,因此在程序中盡量少使用,一般使用更強的線程封閉技術,比如棧封閉或者ThreadLocal類。

2、棧封閉  

  棧封閉是線程封閉的一種特列,在棧封閉中,只能通過局部變量才能訪問對象。局部變量的固有屬性之一就是封閉在執行棧中,其他線程無法訪問這個棧,棧封閉也稱為線程內部使用或者線程局部使用。簡單的說就是局部變量。多個線程訪問一個方法,此方法中的局部變量都會被拷貝一分兒到線程棧中。所以局部變量是不被多個線程所共享的,也就不會出現並發問題。所以能用局部變量就別用全局的變量,全局變量容易引起並發問題。

  比如下面的例子:

 1 public int loadTheArk(Collection<Animal> candidates) {  
 2         SortedSet<Animal> animals;  
 3         int numPairs = 0;  
 4         Animal candidate = null;  
 5           
 6         //animals被封裝在方法中,不要使它們溢出  
 7         animals = new TreeSet<Animal>(new SpeciesGenderComparator());  
 8         animals.addAll(candidates);  
 9         for(Animal a:animals){  
10             if(candidate==null || !candidate.isPotentialMate(a)){  
11                 candidate = a;  
12             }else{  
13                 ark.load(new AnimalPair(candidate,a));  
14                 ++numPairs;  
15                 candidate = null;  
16             }  
17         }  
18         return numPairs;  
19
View Code

  在loadTheArk中實例化一個TreeSet對象,並將該對象的一個引用保存到animals中。此時,只有一個引用指向集合animals,這個引用被封閉到局部變量中,因此也被封閉到局部變量中。然而,如果發布了對集合animals(或者該對象中的任何內部數據)的引用,那么封閉性將被破壞,並導致對象animals的逸出。

3、ThreadLocal類

  維持線程封閉性的一種更加規范方法是使用ThreadLocal類,這個類能使線程中某個值與保存值的對象關聯起來。ThreadLocal類提供了get和set等訪問接口或者方法,這些方法為每個使用該變量的線程都存在一份獨立的副本,因此get總是放回當前執行線程在調用set設置的最新值。看一下下面代碼例子:

 1 public class ConnectionManager {    
 2     private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {  
 3         public Connection initialValue() {  
 4             Connection conn = null;  
 5             try {  
 6                 conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "username", "password"); 
 7             } catch (SQLException e) {  
 8                 e.printStackTrace();  
 9             }  
10             return conn;  
11         }  
12     };  
13   
14     public static Connection getConnection() {  
15         return connectionHolder.get();  
16     }  
17   
18     public static void setConnection(Connection conn) {  
19         connectionHolder.set(conn);  
20     }  
21 }
View Code

  通過調用ConnectionManager.getConnection()方法,每個線程獲取到的,都是自己獨立擁有的一個的Connection對象副本,第一次獲取時,是通過initialValue()方法的返回值來設置值的。通過ConnectionManager.setConnection(Connection conn)方法設置的Connection對象,也只會和當前線程綁定。這樣就實現了Connection對象在多個線程中的完全隔離。在Spring容器中管理多線程環境下的Connection對象時,采用的思路和以上代碼非常相似。

  每個線程是怎么和Connection對象副本綁定的?這個對象副本保存在哪里。當某個線程初次調用ThreadLocal類的get方法時,就會調用initialValue來獲取初始值,從概念上看,我們可以將ThreadLocal<T>視為包含了Map<thread, T>對象,其中保存了特定於該線程的值,但是ThreadLocal的實現並非如此,這樣只是為了我們方便理解而已。

  下面我們來分析一下ThreadLocal的源碼。ThreadLocal類的方法很簡單,只有四個,分別為set,get,remove, initialValue,從字面上我們也能理解這些方法的作用。

  public T get():返回當前線程所對應的局部變量。

  public void set(T arg0):設置當前線程局部變量的值。  

  public void remove():將當前線程局部變量的值刪除,目的是為了減少內存的占用,該方法是JDK 5.0新增的方法。注意,當線程結束后,對應該線程的局部變量將自動被垃圾回收,所以顯式調用該方法清除線程的局部變量並不是必須的操作,但它可以加快內存回收的速度。

  protected T initialValue(): 對當線程局部變量進行初始化,並返回該初始值。是protected 屬性,顯然是讓子類進行對其覆蓋重寫的,只有第一次調用set和get方法時才調用。  

  下面我們對這四個方法的源碼進行分析,看看ThreadLocal類是如何實現這種“為每個線程提供不同的變量拷貝”。

3.1 set方法

  以下是set方法的源碼

 1 public void set(T arg0) {
 2         Thread arg1 = Thread.currentThread();
 3         ThreadLocal.ThreadLocalMap arg2 = this.getMap(arg1);
 4         if (arg2 != null) {
 5             arg2.set(this, arg0);
 6         } else {
 7             this.createMap(arg1, arg0);
 8         }
 9 
10     }
View Code

  從set方法中可以看到,首先獲取當前線程:Thread arg1 = Thread.currentThread();

  再獲取當前線程的ThreadLocalMap:ThreadLocal.ThreadLocalMap arg2 = this.getMap(arg1);

  判斷ThreadLocalMap是否為空,不為空,則以鍵值對的形式設置值,key為this,value就是局部變量的副本,this是當前線程持有的ThreadLocal類實例化對象。

  假如為空,則通過createMap方法創建。

  我們看下getMap和createMap方法的源碼:

1 ThreadLocal.ThreadLocalMap getMap(Thread arg0) {
2         return arg0.threadLocals;
3 }
4 
5 
6 void createMap(Thread arg0, T arg1) {
7         arg0.threadLocals = new ThreadLocal.ThreadLocalMap(this, arg1);
8     
9 }
View Code

  從代碼上已經寫的非常清楚,每個線程都有自己的局部變量的副本,該副本是存在ThreadLocalMap 中,其中鍵值就是ThreadLocal類實例化對象。也就是說每個線程都擁有自己的ThreadLocalMap,ThreadLocalMap保存的就是局部變量副本。我們看一下java.lang.Thread源碼。

1 private static int threadInitNumber;
2 ThreadLocalMap threadLocals = null;
3 ThreadLocalMap inheritableThreadLocals = null;
View Code

3.2 get方法

 1 public T get() {
 2         Thread arg0 = Thread.currentThread();
 3         ThreadLocal.ThreadLocalMap arg1 = this.getMap(arg0);
 4         if (arg1 != null) {
 5             ThreadLocal.ThreadLocalMap.Entry arg2 = arg1.getEntry(this);
 6             if (arg2 != null) {
 7                 Object arg3 = arg2.value;
 8                 return arg3;
 9             }
10         }
11 
12         return this.setInitialValue();
13 }
View Code

  從代碼上看,前兩步和set方法是一個樣的,分別獲取當前線程和當前線程的ThreadLocalMap,第三步判斷ThreadLocalMap是否為空,不為空根據this鍵值獲取value,為空調用setInitialValue()方法。

  以下是setInitialValue方法代碼:

 1 private T setInitialValue() {
 2         Object arg0 = this.initialValue();
 3         Thread arg1 = Thread.currentThread();
 4         ThreadLocal.ThreadLocalMap arg2 = this.getMap(arg1);
 5         if (arg2 != null) {
 6             arg2.set(this, arg0);
 7         } else {
 8             this.createMap(arg1, arg0);
 9         }
10 
11         return arg0;
12 }
View Code

  在setInitialValue里調用了initialValue()方法,也就是子類要重寫覆蓋的方法,對應上面的例子的代碼是:

1 protected Connection initialValue() {  
2             Connection conn = null;  
3             try {  
4                 conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "username", "password"); 
5             } catch (SQLException e) {  
6                 e.printStackTrace();  
7             }  
8             return conn;  
9 }  
View Code

  然后獲取當前線程和當前線程的ThreadLocalMap,ThreadLocalMap為空則調用createMap,否則調用set方法。

3.3 總結

  ThreadLocalMap對象是以this指向的ThreadLocal對象為鍵進行查找的,這當然和前面set()方法的代碼是相呼應的。

  進一步地,我們可以創建不同的ThreadLocal實例來實現多個變量在不同線程間的訪問隔離,為什么可以這么做?因為不同的ThreadLocal對象作為不同鍵,當然也可以在線程的ThreadLocalMap對象中設置不同的值了。通過ThreadLocal對象,在多線程中共享一個值和多個值的區別,就像你在一個HashMap對象中存儲一個鍵值對和多個鍵值對一樣,僅此而已。

  也就說,每個線程都有一個ThreadLocalMap,該線程訪問到某個局部變量,且該局部變量是用ThreadLocal類進行聲明時,該線程就會new ThreadLocal(),然后將該ThreadLocal類的對象作為key值,所對應的局部變量作為value值保存到ThreadLocalMap中。當線程訪問多個ThreadLocal類進行聲明局部變量時,在ThreadLocalMap中就有多個鍵值對。而每個線程都有自己的ThreadLocalMap,從而達到隔離的目的了。

  當某個線程終止后,該線程里的ThreadLocalMap也被回收了,所以完全不用擔心內存泄漏的問題。

  假如多線程訪問的對象實例是單例的,或者說只能創建一個,那就老老實實的使用同步機制(synchronized)了.

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM