我如何使用ThreadLocal

寫這篇的緣由是看到一篇探討ThreadLocal內存泄露問題的文章。不管是做者引用的別人的代碼,仍是做者本身寫的代碼,對ThreadLocal的使用都讓我很驚訝(呃。。老外的文章通常都會這麼說)。由於我是特別關注過ThreadLocal的,我在看書的時候曾把它看成一種過期的東西(書中彷佛是這麼說的),而實踐過程當中發現它倒是個很好用。 java

首先看下Java6API文檔中對它的描述(中文版): 安全

該類提供了線程局部 (thread-local) 變量。這些變量不一樣於它們的普通對應物,由於訪問某個變量(經過其 get 或 set 方法)的每一個線程都有本身的局部變量,它獨立於變量的初始化副本。ThreadLocal 實例一般是類中的 private static 字段,它們但願將狀態與某一個線程(例如,用戶 ID 或事務 ID)相關聯。 例如,如下類生成對每一個線程惟一的局部標識符。線程 ID 是在第一次調用 UniqueThreadIdGenerator.getCurrentThreadId() 時分配的,在後續調用中不會更改。
import java.util.concurrent.atomic.AtomicInteger;

public class UniqueThreadIdGenerator {

     private static final AtomicInteger uniqueId = new AtomicInteger(0);

     private static final ThreadLocal < Integer > uniqueNum = 
         new ThreadLocal < Integer > () {
             @Override protected Integer initialValue() {
                 return uniqueId.getAndIncrement();
         }
     };
 
     public static int getCurrentThreadId() {
         return uniqueId.get();
     }
 } // UniqueThreadIdGenerator

每一個線程都保持對其線程局部變量副本的隱式引用,只要線程是活動的而且 ThreadLocal 實例是可訪問的;在線程消失以後,其線程局部實例的全部副本都會被垃圾回收(除非存在對這些副本的其餘引用)。

下面是我寫的有關ThreadLocal內存回收的測試代碼: 多線程

package misty.threadlocal;

import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * Created with IntelliJ IDEA.
 * User: Misty
 * Date: 13-3-14
 * Time: 上午8:59
 */
public class ThreadLocalTest {
	private final static ThreadLocal<My50MB> tl = new ThreadLocal<My50MB>() {
		@Override
		protected My50MB initialValue() {
			System.out.println(new Date() + " - init thread local value. in " + Thread.currentThread());
			return new My50MB();
		}
	};

	public static void main(String[] args) throws InterruptedException {
		Thread t = new Thread(new Runnable() {
			@Override
			public void run() {
				My50MB m = tl.get();
				// do anything with m
			}
		}, "one");
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				My50MB m = tl.get();
				// do anything with m
				try {
					TimeUnit.SECONDS.sleep(1);
				} catch (InterruptedException e) {
					// ignore
				}
			}
		}, "two");
		t.start();
		t2.start();
		t.join();
		System.gc();
		System.out.println(new Date() + " - GC");
		t2.join();
		System.gc();
		System.out.println(new Date() + " - GC");
		TimeUnit.MILLISECONDS.sleep(100);
	}

	public static class My50MB {
		private String name = Thread.currentThread().getName();
		private byte[] a = new byte[1024 * 1024 * 50];

		@Override
		protected void finalize() throws Throwable {
			System.out.println(new Date() + " - My 50 MB finalized. in " + name);
			super.finalize();
		}
	}
}
//輸出
//Thu Mar 14 09:29:14 CST 2013 - init thread local value. in Thread[two,5,main]
//Thu Mar 14 09:29:14 CST 2013 - init thread local value. in Thread[one,5,main]
//Thu Mar 14 09:29:14 CST 2013 - GC
//Thu Mar 14 09:29:14 CST 2013 - My 50 MB finalized. in one
//Thu Mar 14 09:29:15 CST 2013 - GC
//Thu Mar 14 09:29:15 CST 2013 - My 50 MB finalized. in two 

那麼ThreadLocal有何用呢? dom

不少時候咱們會建立一些靜態域來保存全局對象,那麼這個對象就可能被任意線程訪問到,若是它是線程安全的,這固然沒什麼說的。然而大部分狀況下它不是線程安全的(或者沒法保證它是線程安全的),尤爲是當這個對象的類是由咱們本身(或身邊的同事)建立的(不少開發人員對線程的知識都是隻知其一;不知其二,更況且線程安全)。 ide

這時候咱們就須要爲每一個線程都建立一個對象的副本。咱們固然能夠用ConcurrentMap<Thread, Object>來保存這些對象,但問題是當一個線程結束的時候咱們如何刪除這個線程的對象副本呢? 測試

ThreadLocal爲咱們作了一切。首先咱們聲明一個全局的ThreadLocal對象(final static,沒錯,我很喜歡final),當咱們建立一個新線程並調用threadLocal.get時,threadLocal會調用initialValue方法初始化一個對象並返回,之後不管什麼時候咱們在這個線程中調用get方法,都將獲得同一個對象(除非期間set過)。而若是咱們在另外一個線程中調用get,將的到另外一個對象,並且始終會獲得這個對象。 atom

當一個線程結束了,ThreadLocal就會釋放跟這個線程關聯的對象,這不須要咱們關心,反正一切都悄悄地發生了。 spa

(以上敘述只關乎線程,而不關乎get和set是在哪一個方法中調用的。之前有不少不理解線程的同窗老是問我這個方法是哪一個線程,那個方法是哪一個線程,我不知如何回答。) 線程

下面咱們從實際角度出發。 code

假如咱們須要在多線程環境下生成大量隨機數,咱們不會但願每次都建立一個Random對象,而後丟棄:

public void test1() {
	Random r = new Random();
	int i = r.nextInt();
	//use i
}
最多見的作法是建立一個全局Random對象:
public final static Random rand = new Random();

public void test2() {
	int i = rand.nextInt();
	// use i
}

 幸運的是Random類的線程安全的,咱們能夠肆無忌憚的在多個線程中使用它。

然而在另外一個需求中,咱們就沒那麼幸運了——日期格式化DataFormat——咱們天天都在跟日期格式化打交道,一樣不少人是這麼作的:

//第一種,效率低下
public void test1() {
	DateFormat format = new SimpleDateFormat("yyyy-MM-dd");
	System.out.println(format.format(new Date()));
}
//第二種,單線程沒問題,多線程等着哭吧
private final static DateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd");
public void test2() {
	System.out.println(FORMAT.format(new Date()));
}
//第三種,這是我本身加的,兩年的工做中沒見過這種寫法,由於身邊的同事會用synchronized關鍵字的都屈指可數
//這種方式依舊是效率低下(可能還不及第一種,沒具體測試過)
public void test3() {
    synchronized (FORMAT) {
        System.out.println(FORMAT.format(new Date()));
    }
}
DateFormat的API寫得很清楚:
同步 日期格式不是同步的。建議爲每一個線程建立獨立的格式實例。若是多個線程同時訪問一個格式,則它必須保持外部同步。


保持外部同步已經在上述第三種方法中試過了,那麼咱們就爲每一個線程建立獨立實例吧。

public final static ThreadLocal<DateFormat> TL_FORMAT = new ThreadLocal<DateFormat>() {
	@Override
	protected DateFormat initialValue() {
		return new SimpleDateFormat("yyyy-MM-dd");
	}
};
public void test4() {
	DateFormat df = TL_FORMAT.get();
	System.out.println(df.format(new Date()));
}
關於ThreadLocal的用法暫時探討到這。
相關文章
相關標籤/搜索