一、線程局部變量ThreadLocal
ThreadLocal為變量在每個線程中都建立了一個副本,那麼每個線程可以通路自己内部的副本變量。既然是隻有目前線程可以通路的資料,自然是線程安全的。
主要方法:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyNzkDMwIjMxIjNxcDM3EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
initialValue()方法可以重寫,它預設是傳回null。
下面來看一個例子:
public class ThreadLocalTest {
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-07-16 10:34:" + 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<10;i++){
es.execute(new ParseDate(i));
}
es.shutdown();
}
}
執行上面的程式可以回得到下面的異常:
因為SimpleDateFormat.parse方法并不是線程安全的,是以線上程池中共享這個對象必然導緻錯誤。
一種可行的方法就是加鎖:
public class ThreadLocalTest {
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;}
private static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
try{
lock.lock();
Date t = sdf.parse("2017-07-16 10:34:" + i % 60);
System.out.println(i + ":" + t);
lock.unlock();
}catch (ParseException e){
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(10);
for(int i = 0;i<10;i++){
es.execute(new ParseDate(i));
}
es.shutdown();
}
}
運作結果:
我們也可以用ThreadLocal為每一個線程都産生一個SimpleDateFormat對象執行個體:
public class ThreadLocalTest {
static ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<>();
public static class ParseDate implements Runnable{
int i = 0;
public ParseDate(int i){this.i = i;}
@Override
public void run() {
try{
if(tl.get() == null){
tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
Date t = tl.get().parse("2017-07-16 10:34:" + 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<10;i++){
es.execute(new ParseDate(i));
}
es.shutdown();
}
}
運作結果也是OK的。
這裡要注意的是:需要自己為每個線程配置設定不同的SimpleDateFormat對象,ThreadLocal隻是起到了簡單的容器的作用。如果在應用上為每一個線程配置設定了相同的對象執行個體,那麼ThreadLocal也不能保證線程安全。
看到這裡可能你會問:上面這個例子,把ThreadLocal換成,直接在ParseDate中建立一個成員變量Private SimpleDateFormat sdf = new SimpleDateFormat(“yyyy-MM-dd HH:mm:dd”)不也可以達到同樣的效果。當然這樣效果跟ThreadLocal是一樣的,ThreadLocal隻是提供了一個容器,容納這些需要在每個線程上都互不幹擾的變量的副本。
二、ThreadLocal源碼分析
下面我們來分析下ThreadLocal的源碼,看看是怎麼保證這些對象隻被目前線程所通路。
首先我們要先了解ThreadLocal中的一個靜态内部類:ThreadLocalMap
ThreadLocalMap是一個類似HashMap的東西,更準确的說是WeakHashMap。
進一步檢視ThreadLocalMap的實作,可以看到它由一系列的Entry構成:
static class Entry extends WeakReference<ThreadLocal> {
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
可以看到ThreadLocal的實作使用了弱引用。為什麼要使用弱引用呢?
先看看WeakRefercence的特點:
WeakReference是Java語言規範中為了差別直接的對象引用(程式中通過構造函數聲明出來的對象引用)而定義的另外一種引用關系。WeakReference标志性的特點是:reference執行個體不會影響到被應用對象的GC回收行為(即隻要對象被除WeakReference對象之外所有的對象解除引用後,該對象便可以被GC回收),隻不過在被對象回收之後,reference執行個體想獲得被應用的對象時程式會傳回null。
ThreadLocalMap中的每個Entry都引用了ThreadLocal執行個體,如果ThreadLocal執行個體是強引用,那麼即使把ThreadLocal的執行個體設為null,但這個執行個體在ThreadLocalMap中還有引用,導緻無法被GC回收。聲明為WeakReference的話,ThreadLocal執行個體在ThreadLocalMap中的引用就為弱引用,那麼把ThreadLocal執行個體設為null後,它就可以被GC回收了。當然,如果使用完ThreadLocal執行個體的話,最好是用threadLocal.remove()來代替threadLocal = null。
主要看ThreadLocal的set()和get()方法。
首先我們要知道每個Thread執行個體都有一個ThreadLocalMap類型的成員變量:
ThreadLocal.ThreadLocalMap threadLocals = null;
set()方法:
public void set(T value) {
Thread t = Thread.currentThread(); //拿到目前線程
ThreadLocalMap map = getMap(t); //拿到目前線程t的那個ThreadLocalMap類型的成員變量
if (map != null)
map.set(this, value); //map不為null,就把鍵為該threadLocal的entry的值設定為value
else
createMap(t, value);//map為null,就為目前線程的那個成員變量new一個ThreadLocalMap并加入一個鍵為該threadLocal,值為value的Entry。
}
get()方法:
public T get() {
Thread t = Thread.currentThread(); //拿到目前線程
ThreadLocalMap map = getMap(t); //拿到目前線程的那個ThreadLocalMap類型的成員變量
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);//map不為null,取出鍵為該threadLocal的Entry對象
if (e != null)
return (T)e.value; //存在這個Entry對象,就傳回它的值
}
return setInitialValue(); //map為null,就為目前線程的那個成員變量new一個ThreadLocalMap并加入一個鍵為該threadLocal,值為初始值的Entry
}
總結一下:
1、變量的副本是通過ThreadLocalMap來存儲,鍵為ThreadLocal執行個體(每個線程可以有多個ThreadLocal執行個體),值為變量的值。
2、每個線程都有一個ThreadLocalMap類型的threadLocals 變量,實際也就存儲在這。
3、一般要在get()之前先set(),否則會抛出空指針異常,除非重寫initialValue方法。