詳細領悟ThreadLocal變量

關於對ThreadLocal變量的理解,我今天查看一下午的博客,本身也寫了demo來測試來看本身的理解究竟是不是那麼回事。從看到博客引出不解,到仔細查看ThreadLocal源碼(JDK1.8),我以爲我頗有必要記錄下來我這大半天的收穫,
今天我研究的最多的就是這兩篇文章說理解。我在這裏暫稱爲A文章和B文章。如下是兩篇博文地址,我是在看完A文章後,頗有疑問,特別是在A文章後的各位網頁的評論中,更加堅決我要弄清楚ThreadLocal究竟是怎麼一回事。
A文章:http://blog.csdn.net/lufeng20/article/details/24314381
B文章:http://www.cnblogs.com/dolphin0520/p/3920407.htmlhtml

首先,咱們從字面上的意思來理解ThreadLocal,Thread:線程,這個毫無疑問。那Local呢?本地的,局部的。也就是說,ThreadLocal是線程本地的變量,只要是本線程內均可以使用,線程結束了,那麼相應的線程本地變量也就跟隨着線程消失了。程序員

如下內容是我的參考他人文章,理解總結出來,誤差之處,歡迎指正。安全

全篇包括兩個部分,我但願你們對ThreadLocal源碼已經有必定了解,我在文章中沒有具體分析源碼:多線程

第一部分是說明ThreadLocal不是用來作變量共享的。ide

第二部分是深刻了解ThreadLocal後獲得的結論,談談什麼狀況用ThreadLocal,以及用ThreadLocal有什麼好處。工具

1、ThreadLocal不是用來解決多線程下訪問共享變量問題的

我想你們都知道,多線程狀況下,對共享變量的訪問是須要同步的,否則會引發不可預知的問題。測試

接下來我就是,我極力想要說明的:ThreadLocal不是用來解決這個問題的!!!!! ThreadLocal能夠在本線程持有一個共享變量的副本,對吧。你們都這麼說。this

我舉個栗子,如果在線程的ThreadLocal中set一個程序中惟一的共享變量,該ThreadLocal僅僅是保存了一個共享變量的引用值,共享變量的實例對象在內存中只有一個。spa

下面咱們先測試一下,是否是這樣:
我先定義一個Person類,咱們假定這個Person是要被共享的吧···哈哈(TheradLocal實際上不是這樣用的.net

class Person {
    private String name;
    Person(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    } 
}

而後建立一個target實現Runnable接口:

/**
 * Person 是共享變量
 * @author bubble
 *
 */
class Target implements Runnable {
    private static Person person = new Person("張三");
    public Target() {}
    
    @Override
    public void run() {
    //線程中建立一個ThreadLocal變量,並將共享變量建立一個本線程副本
       ThreadLocal<Person> df = new ThreadLocal<Person>();
       df.set(person);
    //對本線程副本中的值進行改變
       df.get().setName("李四");
       System.out.println("線程" + Thread.currentThread().getName() + "更改ThreadLocal中Person的名字爲:" + df.get().getName());       
    }
    
    public Person getPerson() {
        return person;
    }  
}

最後咱們來測試一下,到底線程中,對共享變量的本地副本是怎麼一回事:

public class ThreadLocalTest {
    public static void main(String[] args) throws InterruptedException {
        Target target = new Target();
        Thread thread = new Thread(target);
        thread.start();    //建立一個線程,改變線程中共享變量的值   
        t1.join();  //等待線程執行完畢
        //主線程訪問共享變量,發現Person的值被改變
         System.out.println("線程" + Thread.currentThread().getName() + "中共享變量Person的名字:" + target.getPerson().getName());
    }      
}

咱們來看看運行結果:

咱們能夠看到,Thread-0線程雖然建立了一個ThreadLocal,而且將共享變量放入,可是線程內改變了共享變量的值,依然會對共享變量自己進行改變。

參考源碼,咱們能夠看到ThreadLocal調用set(T value)方法時,是將調用者ThreadLocal做爲ThreadLocalMap的key值,value做爲ThreadLocalMap的value值。

咱們看看ThradLocal類裏面到底有什麼:

紅色箭頭標註出了四個咱們經常使用的方法,而且ThreadLocal裏定義了一個內部類ThreadLocalMap,可是注意一下,雖然它定義了這樣一個內部類,但ThreadLocal自己真的沒有持有ThreadLocalMap的變量,

這個ThreadLocalMap的持有者是Thread。

因此,文章A中,在開頭說了這樣一段:

ThreadLocal是如何作到爲每個線程維護變量的副本的呢?其實實現的思路很簡單:在ThreadLocal類中有一個Map,用於存儲每個線程的變量副本,Map中元素的鍵爲線程對象,而值對應線程的變量副本。

正確應該是:在Thread類裏面有一個ThreadLocalMap,用於存儲每個線程的變量的引用,這個Map中的鍵爲ThreadLocal對象,而值對應的是ThreadLocal經過set放進去的變量引用。

我在這裏一直強調的是,ThreadLocal經過set(共享變量)而後再經過ThreadLocal方法get的是共享變量的引用!!!  若是多個線程都在其執行過程當中將共享變量加入到本身的ThreadLocal中,那就是每一個線程都持有一份共享變量的引用副本,注意是引用副本,共享變量的實例只有一個。因此,ThreadLocal不是用來解決線程間共享變量的訪問的事兒的。想要控制共享變量在多個線程之間按照程序員想要的方式來進行,那是鎖和線程間通訊的事,和ThreadLocal沒有半毛錢的關係。

總的來講:每一個線程中都有一個本身的ThreadLocalMap類對象,能夠將線程本身的對象保持到其中,各管各的,線程執行期間均可以正確的訪問到本身的對象。

2、ThreadLocal到底該怎麼用

說了這麼多,我以爲仍是舉栗子來講明一下,ThreadLocal到底該怎麼用,有什麼好處。

你們都知道,SimpleDateFomat是線程不安全的,由於裏面用了Calendar 這個成員變量來實現SimpleDataFormat,而且在Parse 和Format的時候對Calendar 進行了修改,calendar.clear(),calendar.setTime(date)。總之在多線程狀況下,如果用同一個SimpleDateFormat是要出問題的。那麼問題來了,爲了線程安全,是否是在每一個線程使用SimpleDateFormat的時候都手動new出來一個新的用?  這得多麻煩啊,通常來講,在開發時,SimpleDateFormat這樣的類咱們是放在工具類裏面的,阿里巴巴Java開發手冊裏面這樣推薦DateUtils:

public class DateUtils {
    public static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd");
        }
    };
}

這裏重寫了initialValue方法,新建了一個SimpleDateFormat對象並返回,這樣咱們就能夠在任何線程任何地方想要執行日期格式化的時候,就能夠像以下方式來執行,而且線程之間互相沒有影響:

 DateUtils.df.get().format(new Date());

咱們來看看爲何這麼作線程之間就沒有影響了。假設如今有線程A和線程B同時執行以上語句,那麼兩個線程是怎麼操做的呢?

線程A:df.get的時候,首先嚐試得到線程A本身ThreadLocalMap,若是是第一次get,因爲咱們沒有set,而是重寫了initialValue方法,因此在A線程第一次get時沒有ThreadLocalMap,這時線程A會

new一個線程A本身的ThreadLocalMap出來,將df(注意df是ThreadLocal變量)做爲這個map的鍵,將initialValue中返回的值(注意是new出來的)做爲map的值。這個時候A線程裏面就有一個ThreadLocalMap了,而且裏面保存了一個SimpleDateFormat的引用。那麼從如今開始,線程A的生存期間,再次調用df.get(),都將得到一個A線程的ThreadLocalMap,而且經過df做爲鍵獲得相應的SimpleDateFormat;

線程B:df.get的時候,首先嚐試得到線程B本身ThreadLocalMap,若是是第一次get,因爲咱們沒有set,而是重寫了initialValue方法,因此在B線程第一次get時沒有ThreadLocalMap,這時線程B會

new一個線程B本身的ThreadLocalMap出來,將df(注意df是ThreadLocal變量,這裏的df和線程A中的df是同一個,可是又有什麼關係呢,map不同)做爲這個map的鍵,將initialValue中返回的值(注意是new出來的,這裏是線程B在執行df.get時本身new出來的,再也不是線程A中的那個了)做爲map的值。這個時候A線程裏面就有一個ThreadLocalMap了,而且裏面保存了一個SimpleDateFormat的引用。那麼從如今開始,線程B的生存期間,再次調用df.get(),都將得到一個B線程的ThreadLocalMap,而且經過df做爲鍵獲得相應的SimpleDateFormat(這裏和線程A中已是另一個不一樣的對象了);

 

這下大概明白爲何說這樣用就線程安全了吧,這裏的線程安全並非指訪問的同一個對象,而是每一個線程建立本身的對象(SimpleDateFormat)來用,各自用各自的,固然線程安全了。。。

固然你們能夠說,這和本身在線程裏面每次用的時候new出來一個有什麼區別呢,對,沒區別,可是這樣方便啊,並且能夠保持線程裏面只有惟一一個SimpleDateFormat對象,你要每用一次new一次,那就消耗內存了撒。可能你會說,那我只new一個,那個方法用的時候經過參數傳遞過去就行。。。。。  不嫌麻煩的話我也無話可說。哈哈。。  然而ThreadLocal卻太方便了。。。   敬仰神人居然能創造出ThreadLocal。這纔是ThreadLocal

 

總結一下:

ThreadLocal真的不是用來解決對象共享訪問問題的,而主要是提供了保持對象的方法和避免參數傳遞的方便的對象訪問方式。 
一、每一個線程中都有一個本身的ThreadLocalMap類對象,能夠將線程本身的對象保持到其中,各管各的,線程能夠正確的訪問到本身的對象。 
二、將一個共用的ThreadLocal靜態實例做爲key(上面得df),將不一樣對象的引用保存到不一樣線程的ThreadLocalMap中,而後在線程生命週期內執行的各處經過這個靜態ThreadLocal實例的get()方法取得本身線程保存的那個對象,避免了將這個對象(指的是SimpleDateFormat)做爲參數傳遞的麻煩。

 

補充一下:

通常狀況下,經過ThreadLocal.set() 到線程中的對象是該線程本身使用的對象,其餘線程是不須要訪問的,也訪問不到的。各個線程中訪問的是不一樣的對象。

 若不用DateUtils工具類,徹底能夠在線程開始的時候這樣執行:

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        ThreadLocal<SimpleDateFormat> df = new ThreadLocal<>();
        df.set(sdf);

而後在線程生命週期的任何地方調用:

 df.get().format(new Date());

效果是同樣的,但是這沒有工具類方便嘛。。。

 

本文我的理解後整理,文章中存在不少表述不清楚的地方,歡迎留言討論。

 參考文章:

 A文章:http://blog.csdn.net/lufeng20/article/details/24314381
 B文章:http://www.cnblogs.com/dolphin0520/p/3920407.html

 C文章:http://www.iteye.com/topic/103804

相關文章
相關標籤/搜索