java多線程之ThreadLocal

在線程同步中,我們可以使用鎖機制,或者通過CAS。但是還有一種方法就是ThreadLocal。這裏先舉一個生活中的例子,

比如,讓100個人填寫個人信息表,如果只有一支筆的話,那麼大家就得挨個填寫,爲了讓每個人都能完成的填寫,我們就需要

保證大家不能哄搶這一支筆,否則誰也填不玩,這時候可能大家可以想到利用鎖機制來控制這支筆。其實從另外一種角度出發,

我們可以每人發一支筆讓他們填寫信息表。如果鎖是第一種思路的話,那麼ThreadLocal就是第二種思路。接下來先看ThreadLocal

的簡單使用:


下面先來看一個簡單的示例:

public class ThreadLocalDemo {
	private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	public static class ParseDate implements Runnable{
		int i = 0;
		public ParseDate(int i){
			this.i = i;
		}
		@Override
		public void run() {
			try{
				Date t = sdf.parse("2017-03-34 17:10:"+i%60);
				System.out.println(i+":"+t);
			}catch(ParseException e){
				e.printStackTrace();
			}
		}
		
	}
	public static void main(String[] args) {
		ExecutorService es = Executors.newFixedThreadPool(10);//這裏使用到線程池,創建含有十個線程的線程池
		for(int i =0;i<1000;i++){
			es.execute(new ParseDate(i));
		}
	}
}
上述代碼在多線程中使用SimpleDateFormat來解析字符串類型的日誌。如果你執行上面代碼,一般來說,你很可能得到一些異常

這裏截取一部分:

Exception in thread "pool-1-thread-116" java.lang.NumberFormatException: For input string: ""
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Long.parseLong(Long.java:453)
出現這些問題的原因,是SimpleDateFormat.parse()方法並不是線程安全的。因此在線程池中共享這個對象必然導致錯誤。

下面我們看看ThreadLocal的解決方法:

public class ThreadLocalDemo {
	static ThreadLocal<SimpleDateFormat> t1 = new ThreadLocal<SimpleDateFormat>();
	public static class ParseDate implements Runnable{
		int i = 0;
		public ParseDate(int i){
			this.i = i;
		}
		@Override
		public void run() {
			try{
				if(t1.get()==null){
					t1.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
				}
				Date t = t1.get().parse("2017-03-34 17:10:"+i%60);
				System.out.println(i+":"+t);
			}catch(ParseException e){
				e.printStackTrace();
			}
		}
		
	}
	public static void main(String[] args) {
		ExecutorService es = Executors.newFixedThreadPool(10);//這裏使用到線程池,創建含有十個線程的線程池
		for(int i =0;i<1000;i++){
			es.execute(new ParseDate(i));
		}
	}
}

結果截取一部分如下:

992:Mon Apr 03 17:10:32 CST 2017
991:Mon Apr 03 17:10:31 CST 2017
989:Mon Apr 03 17:10:29 CST 2017
987:Mon Apr 03 17:10:27 CST 2017
986:Mon Apr 03 17:10:26 CST 2017
984:Mon Apr 03 17:10:24 CST 2017
983:Mon Apr 03 17:10:23 CST 2017
999:Mon Apr 03 17:10:39 CST 2017
在threadlocal的解決方案中10-14行,如果當前線程不持有SimpleDateFormat對象實例。那麼就新建一個並把它設置到當前線程中,

如果已經持有,則直接使用。


ThreadLocal的實現原理:

我們需要關注的是ThreadLocal中的set()方法和get()方法。從set()方法開始說,首先看看set()方法的源碼:

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
我們剖析一下,我們在調用set方法時,首先獲得當前線程對象,然後通過getMap()拿到當前線程的ThreadLocalMap,

(這裏的ThreadLocalMap其實就是線程中的一個成員變量)並將值設入hreadLocalMap中。其實ThreadLocalMap的

實現使用了弱引用,這個概念在《深入理解java虛擬機》有提過,這裏解釋一下,類似Object obj = new Object() 這類引用表示強引用,

而弱引用的強度顧名思義比強引用弱,帶來的結果就是在GC的時候,無論當前內存是否夠用,都會回收掉被弱引用關聯的對象。

ThreadLocalMap之所以採用弱引用其實好處在於垃圾回收。你看上面的代碼可以發現,我們設置在ThreadLocal中的數據,

其實就是寫入到ThreadLocalMap這個map中,這裏的key表示的就是ThreadLocal這個對象實例,value就是我們需要寫入的值。

說到這,我們再提一下ThreadLocalMap採用弱引用爲什麼好處在於垃圾回收。我們一但在我們程序中將ThreadLocal設置爲null的話,

那麼ThreadLocal這個強引用沒了,而ThreadLocalMap中的key也就是ThreadLocal實例又都是弱引用,自然裏面的value就會被垃圾回收。

這其實也是清理ThreadLocal的一種措施。後面還會講到其他清理措施。

下面再來看看get()方法的源碼:

 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();
    }
首先,get()方法先取得當前線程的ThreadLocalMap對象。然後通過將自己作爲key取得內部的實際數據。


在瞭解了ThreadLocal的內部實現後,我們自然後會引出一個問題。那就是這些變量時維護在Thread類內部的,

這也意味着只要線程不退出,對象的引用就會一直存在。這裏先說明一下,線程在退出時會做一些清理工作,其中

就包括ThreadLocalMap的清理。

如果我們使用線程池,那就意味着當前線程未必會退出。如果這樣,將一些打打的對象設置到ThreadLocal中

(它實際是保存在當前線程持有的ThreadLocalMap中),最後可能會造成內存泄漏問題。如果你設置在ThreadLocal

中的對象不再使用了,你需要清理它。有以下兩種方法:

  • 通過ThreadLocal.remove()
  • 就是前面說的設置ThreadLocal的實例爲null




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