java併發編程實戰 ThreadLocal

另一篇博文:Hibernet中的ThreadLocal使用 http://www.cnblogs.com/gnivor/p/4440776.html

參考資料:

http://blog.sina.com.cn/s/blog_7ffb8dd5010146i3.html

http://lavasoft.blog.51cto.com/62575/51926/

 

一、問題的提出
  
 我們知道Session是由SessionFactory負責創建的,而SessionFactory的實現是線程安全的,多個併發的線程可以同時訪問一 個SessionFactory並從中獲取Session實例,而Session不是線程安全的。Session中包含了數 據庫操作相關的狀態信息,那麼說如果多個線程同時使用一個Session實例進行CRUD,就很有可能導致數據存取的混亂,你能夠想像那些你根本不能預測 執行順序的線程對你的一條記錄進行操作的情形嗎?

二、 解決方案思路使用Threadlocal類集合
  早在Java1.2推出之時,Java平臺中就引入了一個新的支持:java.lang.ThreadLocal,給我們在編寫多線程程序時提供了一種新 的選擇。ThreadLocal是什麼呢?其實ThreadLocal並非是一個線程的本地實現版本,它並不是一個Thread,而是thread local variable(線程局部變量)。也許把它命名爲ThreadLocalVar更加合適。線程局部變量(ThreadLocal)其實的功用非常簡單, 就是爲每一個使用該變量的線程都提供一個變量值的副本,是每一個線程都可以獨立地改變自己的副本,而不會和其它線程的副本衝突。從線程的角度看,就好像每 一個線程都完全擁有一個該變量。
  ThreadLocal這個類本身不是代表線程要訪問的變量,這個類的成員變量纔是。JDK1.5給ThreadLocal加了泛型功能,即是 ThreadLocal,這個泛型T即是要線程的本地變量。線程通過ThreadLocal的get和set方法去訪問這個變量T。
  ThreadLocal是如何做到爲每一個線程維護變量的副本的呢?其實實現的思路很簡單,在ThreadLocal類中有一個Map,用於存儲每一個線程的變量的副本。比如下面的示例實現(爲了簡單,沒有考慮集合的泛型):

複製代碼
   public class ThreadLocal { 
         private Map values = Collections.synchronizedMap(new HashMap()); 
         public Object get() { 
           Thread currentThread = Thread.currentThread();  
            Object result = values.get(currentThread);  
            if(result == null&&!values.containsKey(currentThread)) { 
               result = initialValue(); 
               values.put(currentThread, result);  
            } 
           return result;  
        } 
        public void set(Object newValue) { 
           values.put(Thread.currentThread(), newValue); 
        } 
        public Object initialValue() { 
           return null;  
        } 
   }
複製代碼

 

三、解決方案步驟

   1、在HibernateUtil類中我們需要定義一個靜態的成員變量用於保存當前線程共用的Session  

複製代碼
  public class HibernateUtil {
    private static SessionFactory factory;
    // 使用ThreadLocal集合保存當前業務線程中的SESSION
    private static ThreadLocal threadlocal= new ThreadLocal();

    static {
        // 第一步:讀取HIBERNATE的配置文件,讀取hibernate.cfg.xml文件
        Configuration con = new Configuration().configure();
        // 第二步:創建服務註冊構建器對象,通過配置對象中加載所有的配置信息,存放到註冊服務中
        ServiceRegistryBuilder regBuilder = new ServiceRegistryBuilder()
                .applySettings(con.getProperties());
        // 創建註冊服務
        ServiceRegistry reg = regBuilder.buildServiceRegistry();
        // 第三步:創建會話工廠
        factory = con.buildSessionFactory(reg);
    }

   
    public static Session getLocalThreadSession() {
        Session s = threadlocal.get();// 獲取當前線程下的SESSION
        if (s == null) {
            s = getFactory().getCurrentSession();// 獲取當前線程中的SESSION,需在在Hibernate.cfg.xml文件,具體請看面的說明
            threadlocal.set(s);// 將當前SESSION放入到當前線程的容器中保存
        }
        return s;
    }

    
    public static void closeSession() {
        Session s = threadlocal.get();// 獲取當前線程下的SESSION
        if (s != null) {
            // s.close();//這裏無需將Session關閉,因爲該Session是保存在當前線程//中的,線程執行完畢Session自然會銷燬
            threadlocal.set(null);// 將當前線程中的會話清除
        }
    }

}
複製代碼

 

2、添加OpenSessionInViewFilter過濾器(不要忘了在Web.xml配置該過濾器

複製代碼
public void doFilter(ServletRequest request, ServletResponse response,
          FilterChain filterChain) throws IOException, ServletException {
  Session s = HibernateUtil.getThreadLocalSession();
  Transaction t = null;
  try {
    // 開始事務
    t = s.beginTransaction();
    // 進入一系列的過濾鏈,處理相應的ACTION、業務邏輯及數據層
    filterChain.doFilter(request, response);
    // 提交事務
    t.commit();
  } catch (Exception e) {
  if (t != null)
     t.rollback();//出現異常回滾事務
  throw new RuntimeException(e.getMessage(), e);
  } finally {
     HibernateUtil.closeSession();
  }
}
複製代碼

===========================================

說明:關於getCurrentSession()方法:
    sessionFactory.getCurrentSession()獲取當前線程中的Session, 當調用時,hibernate將session綁定到當前線程,事務結束後,hibernate將session從當前線程中釋放,並且關閉 session。當再次調用getCurrentSession()時,將得到一個新的session,並重新開始這一系列工作。這樣調用方法如下: Session session = HibernateUtil.getSessionFactory().getCurrentSession();

getCurrentSession和openSession的區別:
1、getCurrentSession創建的session會和綁定到當前線程,而openSession不會。
2、getCurrentSession創建的線程會在事務回滾或事物提交後自動關閉,而openSession必須手動關閉
3、getCurrentSession需在在Hibernate.cfg.xml文件中添加配置:
  <property name="current_session_context_class">thread</property>

 

四、總結

   這種解決方案的優缺點:

1. 優點:使用ThreadLocal除了有避免頻繁創建和銷燬session的好處外, 還有一個特別大的好處,
就是可以做到多線程的數據隔離, 可以避免多個線程同時操作同一個session

2. 缺點:  如下圖


使用攔截器在響應返回時,又重複過濾了一次,延長了響應的時間(改進:我們可以把寫在過濾器中的方法寫在一個具體的類,用到的時候再調用)

 

五:注意:

ThreadLocal使用場合主要解決多線程中數據因併發產生不一致問題。ThreadLocal爲每個線程的中併發訪問的數據提供一個副本,通過訪問副本來運行業務,這樣的結果是耗費了內存,大大減少了線程同步所帶來性能消耗,也減少了線程併發控制的複雜度。

ThreadLocal不能使用原子類型,只能使用Object類型。ThreadLocal的使用比synchronized要簡單得多。

ThreadLocal和Synchonized都用於解決多線程併發訪問。但是ThreadLocal與synchronized有本質的區別。

1. synchronized是利用鎖的機制,使變量或代碼塊在某一時該只能被一個線程訪問。

2. ThreadLocal爲每一個線程都提供了變量的副本,使得每個線程在某一時間訪問到的並不是同一個對象,這樣就隔離了多個線程對數據的數據共享。而Synchronized卻正好相反,它用於在多個線程間通信時能夠獲得數據共享。

Synchronized用於線程間的數據共享,而ThreadLocal則用於線程間的數據隔離

當然ThreadLocal並不能替代synchronized,它們處理不同的問題域。Synchronized用於實現同步機制,比ThreadLocal更加複雜



本文參考:
http://blog.csdn.net/lufeng20/article/details/24314381
http://www.cnblogs.com/chenying99/articles/3405161.html

  

ThreadLocal類接口很簡單,只有4個方法,我們先來了解一下: 

void set(Object value) 設置當前線程的線程局部變量的值。 
public Object get() 該方法返回當前線程所對應的線程局部變量。 
public void remove() 將當前線程局部變量的值刪除,目的是爲了減少內存的佔用,該方法是JDK 5.0新增的方法。
注意,當線程結束後,對應該線程的局部變量將自動被垃圾回收,所以顯式調用該方法清除線程的局部變量並不是必須的操作,但它可以加快內存回收的速度。 
protected Object initialValue() 返回該線程局部變量的初始值,該方法是一個protected的方法,顯然是爲了讓子類覆蓋而設計的。
這個方法是一個延遲調用方法,在線程第1次調用get()或set(Object)時才執行,並且僅執行1次。ThreadLocal中的缺省實現直接返回一個null。

 

一、知其然

synchronized這類線程同步的機制可以解決多線程併發問題,在這種解決方案下,多個線程訪問到的,都是同一份變量的內容。爲了防止在多線程訪問的過程中,可能會出現的併發錯誤。不得不對多個線程的訪問進行同步,這樣也就意味着,多個線程必須先後對變量的值進行訪問或者修改,這是一種以延長訪問時間來換取線程安全性的策略。

而ThreadLocal類爲每一個線程都維護了自己獨有的變量拷貝。每個線程都擁有了自己獨立的一個變量,競爭條件被徹底消除了,那就沒有任何必要對這些線程進行同步,它們也能最大限度的由CPU調度,併發執行。並且由於每個線程在訪問該變量時,讀取和修改的,都是自己獨有的那一份變量拷貝,變量被徹底封閉在每個訪問的線程中,併發錯誤出現的可能也完全消除了。對比前一種方案,這是一種以空間來換取線程安全性的策略。

來看一個運用ThreadLocal來實現數據庫連接Connection對象線程隔離的例子。

複製代碼
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
  
public class ConnectionManager {    
    private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {  
        @Override  
        protected Connection initialValue() {  
            Connection conn = null;  
            try {  
                conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "username", "password"); 
            } catch (SQLException e) {  
                e.printStackTrace();  
            }  
            return conn;  
        }  
    };  
  
    public static Connection getConnection() {  
        return connectionHolder.get();  
    }  
  
    public static void setConnection(Connection conn) {  
        connectionHolder.set(conn);  
    }  
}  
複製代碼

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

附:另一個例子 

複製代碼
public class TestNum {    
    private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>(){
        public Integer initialValue() { 
            return 0;
        }
    };
    
    public int getNextNum(){
        seqNum.set(seqNum.get()+1);
        return seqNum.get();
    }
    
    public static void main(String[] args) {        
        TestNum sn = new TestNum();        
    
        //三個線程共享SN 產生序列號
        ThreadClient t1 = new ThreadClient(sn);
        ThreadClient t2 = new ThreadClient(sn);
        ThreadClient t3 = new ThreadClient(sn);
        t1.start();
        t2.start();
        t3.start();
    }
}

class ThreadClient extends Thread{
    private TestNum sn ;
    
    public ThreadClient(TestNum sn){
        this.sn = sn;
    }
    public void run(){
        for(int i = 0 ; i < 3 ; i++){
            System.out.println("Thread: "+ Thread.currentThread().getName() 
                    + " sn: " + sn.getNextNum());
        }
    }    
}
複製代碼

  

二、知其所以然

那麼到底ThreadLocal類是如何實現這種“爲每個線程提供不同的變量拷貝”的呢?先來看一下ThreadLocal的set()方法的源碼是如何實現的: 

複製代碼
    /** 
    * Sets the current thread's copy of this thread-local variable 
    * to the specified value.  Most subclasses will have no need to 
    * override this method, relying solely on the {@link #initialValue} 
    * method to set the values of thread-locals. 
    * 
    * @param value the value to be stored in the current thread's copy of 
    *        this thread-local. 
    */  
   public void set(T value) {  
       Thread t = Thread.currentThread();  
       ThreadLocalMap map = getMap(t);  
       if (map != null)  
           map.set(this, value);  
       else  
           createMap(t, value);  
   }  
複製代碼
  在這個方法內部我們看到,首先通過getMap(Thread t)方法獲取一個和當前線程相關的ThreadLocalMap,然後將變量的值設置到這個ThreadLocalMap對象中,當然如果獲取到的ThreadLocalMap對象爲空,就通過createMap方法創建。

  線程隔離的祕密,就在於ThreadLocalMap這個類。ThreadLocalMap是ThreadLocal類的一個靜態內部類,它實現了鍵值對的設置和獲取(對比Map對象來理解),每個線程中都有一個獨立的ThreadLocalMap副本,它所存儲的值,只能被當前線程讀取和修改。ThreadLocal類通過操作每一個線程特有的ThreadLocalMap副本,從而實現了變量訪問在不同線程中的隔離。因爲每個線程的變量都是自己特有的,完全不會有併發錯誤。還有一點就是,ThreadLocalMap存儲的鍵值對中的鍵是this對象指向的ThreadLocal對象,而值就是你所設置的對象了。

爲了加深理解,我們接着看上面代碼中出現的getMapcreateMap方法的實現: 

複製代碼
/** 
 * Get the map associated with a ThreadLocal. Overridden in 
 * InheritableThreadLocal. 
 * 
 * @param  t the current thread 
 * @return the map 
 */  
ThreadLocalMap getMap(Thread t) {  
    return t.threadLocals;  
}  
  
/** 
 * Create the map associated with a ThreadLocal. Overridden in 
 * InheritableThreadLocal. 
 * 
 * @param t the current thread 
 * @param firstValue value for the initial entry of the map 
 * @param map the map to store. 
 */  
void createMap(Thread t, T firstValue) {  
    t.threadLocals = new ThreadLocalMap(this, firstValue);  
}  
複製代碼

 代碼已經說的非常直白,就是獲取和設置Thread內的一個叫threadLocals的變量,而這個變量的類型就是ThreadLocalMap,這樣進一步驗證了上文中的觀點:每個線程都有自己獨立的ThreadLocalMap對象。打開java.lang.Thread類的源代碼,我們能得到更直觀的證明:

/* ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

 

那麼接下來再看一下ThreadLocal類中的get()方法,代碼是這麼說的: 

複製代碼
/** 
 * Returns the value in the current thread's copy of this 
 * thread-local variable.  If the variable has no value for the 
 * current thread, it is first initialized to the value returned 
 * by an invocation of the {@link #initialValue} method. 
 * 
 * @return the current thread's value of this thread-local 
 */  
public T get() {  
    Thread t = Thread.currentThread();  
    ThreadLocalMap map = getMap(t);  
    if (map != null) {  
        ThreadLocalMap.Entry e = map.getEntry(this);  
        if (e != null)  
            return (T)e.value;  
    }  
    return setInitialValue();  
}  
複製代碼

  

再來看setInitialValue()方法: 

複製代碼
/** 
    * Variant of set() to establish initialValue. Used instead 
    * of set() in case user has overridden the set() method. 
    * 
    * @return the initial value 
    */  
   private T setInitialValue() {  
       T value = initialValue();  
       Thread t = Thread.currentThread();  
       ThreadLocalMap map = getMap(t);  
       if (map != null)  
           map.set(this, value);  
       else  
           createMap(t, value);  
       return value;  
   }   
複製代碼

 

這兩個方法的代碼告訴我們,在獲取和當前線程綁定的值時,ThreadLocalMap對象是以this指向的ThreadLocal對象爲鍵進行查找的,這當然和前面set()方法的代碼是相呼應的。

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

設置到這些線程中的隔離變量,會不會導致內存泄漏呢?ThreadLocalMap對象保存在Thread對象中,當某個線程終止後,存儲在其中的線程隔離的變量,也將作爲Thread實例的垃圾被回收掉,所以完全不用擔心內存泄漏的問題。在多個線程中隔離的變量,光榮的生,合理的死,真是圓滿,不是麼?

最後再提一句,ThreadLocal變量的這種隔離策略,也不是任何情況下都能使用的。如果多個線程併發訪問的對象實例只允許,也只能創建一個,那就沒有別的辦法了,老老實實的使用同步機制(synchronized)來訪問吧。

http://my.oschina.net/lichhao/blog/111362

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章