詳細領悟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.html

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

以下內容是個人參考他人文章,理解總結出來,偏差之處,歡迎指正。

全篇包括兩個部分,我希望大家對ThreadLocal源碼已經有一定了解,我在文章中沒有具體分析源碼:

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

第二部分是深入了解ThreadLocal后得到的結論,談談什么情況用ThreadLocal,以及用ThreadLocal有什么好處。

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

我想大家都知道,多線程情況下,對共享變量的訪問是需要同步的,不然會引起不可預知的問題。

接下來我就是,我極力想要說明的:ThreadLocal不是用來解決這個問題的!!!!! ThreadLocal可以在本線程持有一個共享變量的副本,對吧。大家都這么說。

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

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

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類對象,可以將線程自己的對象保持到其中,各管各的,線程執行期間都可以正確的訪問到自己的對象。

二、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真的不是用來解決對象共享訪問問題的,而主要是提供了保持對象的方法和避免參數傳遞的方便的對象訪問方式。 
1、每個線程中都有一個自己的ThreadLocalMap類對象,可以將線程自己的對象保持到其中,各管各的,線程可以正確的訪問到自己的對象。 
2、將一個共用的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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM