1. 程式人生 > >Java學習整理系列之ThreadLocal的理解

Java學習整理系列之ThreadLocal的理解

ThreadLocal概念

ThreadLocal是解決執行緒安全問題一個很好的思路,它通過為每個執行緒提供一個獨立的變數副本解決了變數併發訪問的衝突問題。在很多情況下,ThreadLocal比直接使用synchronized同步機制解決執行緒安全問題更簡單,更方便,且結果程式擁有更高的併發性。

ThreadLocal與Connection

疑問1:有一個使用者請求就會啟動一個執行緒。而如果ThreadLocal用的是變數副本,那我們把connection放在Threadlocal裡的話,那麼我們的程式只需要一個connection連線資料庫就行了,每個執行緒都是用的connection的一個副本,那為什麼還有必要要資料庫連線池呢?

ThreadLocal使得各執行緒能夠保持各自獨立的一個物件,並不是通過ThreadLocal.set()來實現的,而是通過每個執行緒中的new 物件 的操作來建立的物件,每個執行緒建立一個,不是什麼物件的拷貝或副本。通過ThreadLocal.set()將這個新建立的物件的引用儲存到各執行緒的自己的一個map中,每個執行緒都有這樣一個map,執行ThreadLocal.get()時,各執行緒從自己的map中取出放進去的物件,因此取出來的是各自自己執行緒中的物件,ThreadLocal例項是作為map的key來使用的。

疑問2:既然ThreadLocal噹噹前執行緒中沒有時去新建一個新的,有的話就用當前執行緒中的,那資料庫連線池已經有了這種功能啊,還要ThreadLocal幹什麼?


由於請求中的一個事務涉及多個 DAO 操作,而這些 DAO 中的 Connection 不能從連線池中獲得,如果是從連線池獲得的話,兩個 DAO 就用到了兩個Connection,這樣的話是沒有辦法完成一個事務的。DAO 中的 Connection 如果是從 ThreadLocal 中獲得 Connection 的話那麼這些 DAO 就會被納入到同一個 Connection 之下。當然了,這樣的話,DAO 中就不能把 Connection 給關了,關掉的話,下一個使用者就不能用了。

ThreadLocal與同步機制

在同步機制中,通過物件的鎖機制保證同一時間只有一個執行緒訪問變數。這時該變數是多個執行緒共享的,使用同步機制要求程式慎密地分析什麼時候對變數進行讀寫,什麼時候需要鎖定某個物件,什麼時候釋放物件鎖等繁雜的問題,程式設計和編寫難度相對較大。而ThreadLocal則從另一個角度來解決多執行緒的併發訪問。ThreadLocal會為每一個執行緒提供一個獨立的變數副本(每個執行緒建立一個,不是什麼物件的拷貝或副本

),從而隔離了多個執行緒對資料的訪問衝突。因為每一個執行緒都擁有自己的變數副本,從而也就沒有必要對該變數進行同步了。ThreadLocal提供了執行緒安全的共享物件,在編寫多執行緒程式碼時,可以把不安全的變數封裝進ThreadLocal。概括起來說,對於多執行緒資源共享的問題,同步機制採用了“以時間換空間”的方式,而ThreadLocal採用了“以空間換時間”的方式。前者僅提供一份變數,讓不同的執行緒排隊訪問,而後者為每一個執行緒都提供了一份變數,因此可以同時訪問而互不影響。概括起來說,對於多執行緒資源共享的問題,同步機制採用了“以時間換空間”的方式,而ThreadLocal採用了“以空間換時間”的方式。前者僅提供一份變數,讓不同的執行緒排隊訪問,而後者為每一個執行緒都提供了一份變數,因此可以同時訪問而互不影響。

框架中使用ThreadLocal解決執行緒安全問題

我們知道在一般情況下,只有無狀態的Bean才可以在多執行緒環境下共享,在Spring中,絕大部分Bean都可以宣告為singleton作用域。就是因為Spring對一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非執行緒安全狀態採用ThreadLocal進行處理,讓它們也成為執行緒安全的狀態,因此有狀態的Bean就可以在多執行緒中共享了。一般的Web應用劃分為展現層、服務層和持久層三個層次,在不同的層中編寫對應的邏輯,下層通過介面向上層開放功能呼叫。在一般情況下,從接收請求到返回響應所經過的所有程式呼叫都同屬於一個執行緒。同一執行緒貫通三層這樣你就可以根據需要,將一些非執行緒安全的變數以ThreadLocal存放,在同一次請求響應的呼叫執行緒中,所有關聯的物件引用到的都是同一個變數。

我們在使用Hibernate的時候,經常會使用到currentSession(),而Hibernat是這樣使用ThreadLocal的。

public static final ThreadLocal session = new ThreadLocal();   
    public static Session currentSession() {
        Session s = (Session)session.get(); 
        // open a new session,if this session has none  
        if(s == null){   
              s = sessionFactory.openSession(); 
              session.set(s);   
        }   
          return s;   
    }   
(1)初始化一個ThreadLocal物件,ThreadLocal有三個成員方法 get()、set()、initialvalue()。 如果不初始化initialvalue,則initialvalue返回null。(2)session的get根據當前執行緒返回其對應的執行緒內部變數,也就是我們需要的net.sf.hibernate.Session(相當於對應每個資料庫連線).多執行緒情況下共享資料庫連結是不安全的。ThreadLocal保證了每個執行緒都有自己的s(資料庫連線)。(3)如果是該執行緒初次訪問,自然,s(資料庫連線)會是null,接著建立一個Session,具體就是行6。 (4)建立一個數據庫連線例項 s (5)儲存該資料庫連線s到ThreadLocal中。 (6)如果當前執行緒已經訪問過資料庫了,則從session中get()就可以獲取該執行緒上次獲取過的連線例項。

在自己的專案中使用ThreadLocal解決執行緒安全問題

這裡已request物件為例,首先我們新建一個常量類用來新建ThreadLocal物件,程式碼如下:

public class CommonConstant {
	public static final ThreadLocal<HttpServletRequest> requestTL = new ThreadLocal<HttpServletRequest>(); // 儲存request的threadlocal
}

接著新建一個Request工具類,用來獲得當前的request或取得request裡面的屬性等,程式碼如下:
public class RequestUtil {
	public static Object getAttribute(String name) {
		return CommonConstant.requestTL.get().getAttribute(name);
	}
	public static void setAttribute(String name, Object value) {
		CommonConstant.requestTL.get().setAttribute(name, value);
	}
	public static void removeAttribute(String name) {
		CommonConstant.requestTL.get().removeAttribute(name);
	}
	public static boolean containsKey(String name) {
		Object value = getAttribute(name);
		if (value != null) {
			return true;
		}
		return false;
	}
	public static boolean notContainsKey(String name) {
		Object value = getAttribute(name);
		if (value == null) {
			return true;
		}
		return false;
	}
	public static HttpServletRequest getRequest() {
		return CommonConstant.requestTL.get();
	}
	public static String getParameter(String name) {
		return CommonConstant.requestTL.get().getParameter(name);
	}
}

接著最重要的就是什麼時候將這個request放入當前執行緒,比如在Servlet中當然可以在dosomething(HttpServletRequest request, HttpServletResponse response){}這樣的方法裡,當然也可以再一些攔截器,過濾器的時候進行設定,如Spring的preHandle(HttpServletRequest request, HttpServletResponse response, Object handler),程式碼如下:
public boolean preHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
		CommonConstant.requestTL.set(request);
	}

那麼接下來在需要的地方如Service層中就可以這樣獲取到當前執行緒的request了,程式碼如下:
RequestUtil.getRequest();
RequestUtil.getAttribute(name);
RequestUtil.getParameter(name);
//如果沒有RequestUtil,那麼就先取得request
HttpServletRequest request = CommonConstant.requestTL.get();

ThreadLocal簡單實現

大概思路就是ThreadLocal物件中有一個map,map中儲存的鍵值對的key是當前執行緒,值是執行緒區域性變數的值。

public class ThreadLocal { 
    private Map values = Collections.synchronizedMap(new HashMap());

    public Object get() {
        Thread curThread = Thread.currentThread();
        Object o = values.get(curThread);
        if (o == null && !values.containsKey(curThread)) {
            o = initialValue();
            values.put(curThread, o);
        }
        return o;
    }

    public void set(Object newValue) {
        values.put(Thread.currentThread(), newValue);
    }

    public Object initialValue() {
        return null;
    }
}
這個實現和JDK的總體思路類似,但存在很多問題。因為用 Thread 物件做 values 對映表中的key將導致無法線上程退出後對 Thread 進行垃圾回收,而且也無法儲存多個執行緒區域性變數。從JDK5.0的原始碼來看,並非在ThreadLocal中有一個Map,而是在每個Thread中存在這樣一個Map,具體是ThreadLocal.ThreadLocalMap。當用set時候,往當前執行緒裡面的Map裡 put 的key是當前的ThreadLocal物件。而不是把當前Thread作為Key值put到ThreadLocal中的Map裡。 

ThreadLocal原始碼實現

首先我們來看看JDK中的原始碼,部分原始碼如下:

public class ThreadLocal<T> {
    protected T initialValue() {
        return null;
    }
    public ThreadLocal() {
    }
    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();
    }
    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;
    }
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
}

從public void set(T value){}可以看出在設定當前執行緒的執行緒區域性變數時,首先獲得當前的執行緒,接著獲取一個與當前執行緒關聯的ThreadLocalMap物件(ThreadLocalMap是ThreadLocal類的一個靜態內部類,它實現了鍵值對的設定和獲取,可以用常用的Map物件來理解),如果該ThreadLocalMap物件存在,則將當前執行緒區域性變數和其值放進map,否則的話新建一個map,並使執行緒的threadLocals為新的map的引用。而當我們通過public T get(){}獲取當前執行緒的執行緒區域性變數時,如果當前執行緒有相應的ThreadLocalMap物件,則已執行緒區域性變數為鍵,取出其值,如果map不存在,則通過setInitialValue();將初始化值也就是null設定到map中,並返回該初始化值。

這裡我想強調的是每一個Thread物件中有一個ThreadLocal.ThreadLocalMap threadLocals物件引用,指向的是一個ThreadLocalMap物件,該map用來儲存一些鍵值對,如<requestTL,request>;<reponseTL,reponse>,鍵值對中前一個為定義的執行緒區域性變數,後一個為具體儲存的變數。

public static final ThreadLocal<HttpServletRequest> requestTL = new ThreadLocal<HttpServletRequest>(); // 儲存request的threadlocal
public static final ThreadLocal<HttpServletResponse> responseTL = new ThreadLocal<HttpServletResponse>(); // 儲存response的threadlocal