多線程之線程局部變量ThreadLocal及原理

一、線程局部變量ThreadLocal

ThreadLocal爲變量在每個線程中都創建了一個副本,那麼每個線程可以訪問自己內部的副本變量。既然是隻有當前線程可以訪問的數據,自然是線程安全的。


主要方法:


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方法。

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