ThreadLocal基本原理及運用

ThreadLocal簡述

下面咱們看一下ThreadLocal類的官方註釋。java

  • This class provides thread-local variables. These variables differ from數據庫

  • their normal counterparts in that each thread that accesses one (via its安全

  • {@code get} or {@code set} method) has its own, independently initialized多線程

  • copy of the variable. {@code ThreadLocal} instances are typically private框架

  • static fields in classes that wish to associate state with a thread (e.g.,socket

  • a user ID or Transaction ID).ide

大體的意思是,ThreadLocal提供本地線程變量。這個變量裏面的值(經過get方法獲取)是和其餘線程分割開來的,變量的值只有當前線程能訪問到,不像通常的類型好比Person,Student類型的變量,只要訪問到聲明該變量的對象,便可訪問其所有內容,並且各個線程的訪問的數據是無差異的。Thread的典型應用是提供一個與程序運行狀態相關靜態變量,好比一次訪問回話的表示符號:USERID,或者一次事務裏面的事務id:Transaction ID。學習

基本原理

線程本地變量是和線程相關的變量,一個線程則一份數據。咱們經過ThreadLocal保存的數據最終是保存在Thread類的ThreadLocalMap threadLocals變量中。ThreadlocalMap是一個Map結構,其中key爲咱們聲明的ThreadLocal對象,value即爲咱們使用ThreadLocal保存的線程本地變量. 測試

當咱們調用ThreadLocal變量set方法時,那麼爲將TheadLocal做爲key,set方法的參數作爲value保存在當前線程的threadLocals中.調用get方法時相似,調用get方法時,會去Thread的threadLocals中去尋找key爲ThreadLocal 變量的值this

源碼以下:

//Thread.threadLocals變量聲明
/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. 
 */
ThreadLocal.ThreadLocalMap threadLocals = null;

// ThreadLocal set get方法

/**
 * Sets the current thread's copy of this thread-local variable
 * to the specified value.  Most subclasses will have no need to
 * override this method, relying solely on the {@link #initialValue}
 * method to set the values of thread-locals.
 *
 * @param value the value to be stored in the current thread's copy of
 *        this thread-local.
 */
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);// getMap方法即去獲取當前線程的ThreadLocalMap變量。
    if (map != null)
        map.set(this, value);//以this(ThreadLocal自己)爲Key,參數value爲值進行保存
    else
        createMap(t, value);
}

/**
 * Get the map associated with a ThreadLocal. Overridden in
 * InheritableThreadLocal.
 *
 * @param  t the current thread
 * @return the map
 */
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}


/**
 * Returns the value in the current thread's copy of this
 * thread-local variable.  If the variable has no value for the
 * current thread, it is first initialized to the value returned
 * by an invocation of the {@link #initialValue} method.
 *
 * @return the current thread's value of this thread-local
 */
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

下面是測試代碼:

static ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();

@Test
public  void test01(){
    Thread thread1 = new Thread(){
        @Override
        public void run() {
            stringThreadLocal.set("threadName===>"+Thread.currentThread().getName());
            System.out.println(this.getName()+" thread get the value:"+stringThreadLocal.get());

        }
    };
    Thread thread2 = new Thread(){
        @Override
        public void run() {
            stringThreadLocal.set("threadName===>"+Thread.currentThread().getName());
            System.out.println(this.getName()+" thread get the value:"+stringThreadLocal.get());

        }
    };
    Thread thread3 = new Thread(){
        @Override
        public void run() {
            stringThreadLocal.set("threadName===>"+Thread.currentThread().getName());
            System.out.println(this.getName()+" thread get the value:"+stringThreadLocal.get());
        }
    };

    thread1.start();
    thread2.start();
    thread3.start();
    System.out.println("main線程調用set方法以前:"+stringThreadLocal.get());
    stringThreadLocal.set("main 線程set的值");
    System.out.println("main線程調用set方法以後:"+stringThreadLocal.get());
}

能夠看到不一樣線程設置的值在該線程是可以正確的取到。因爲Thread的threadLocals變量只能在Thread所在的包下才可以訪問,所以不能對該變量進行直接訪問以驗證設置的值在Thread.currentThread對象裏面。但若是你調試以上代碼,設置值以後訪問Thread.currentThread.threadLocals會看到以前設置的值。其中key爲聲明的ThreadLocal對象。

ThreadLocal進行參數傳遞

這算是比較正統的ThreadLocal用法,這可能也是ThreadLocal設計的初衷:用來保存於狀態相關的變量,好比訪問者的用戶信息,事務的事務標識。這裏演示一下使用ThreadLocal來傳遞用戶信息,實際上當前流行的大部分權限框架都是使用的ThreadLocal變量來保存用戶信息的。

下面是測試代碼:

//參數傳遞測試

    @Test
    public void test02(){
        //參數主要利用ThreadLocal是線程的局部變量,只要在同一個線程中,以前設置的值後面就能取到,從而達到參數值傳遞的效果。
        //在前面在線程變量中添加值
        stringThreadLocal.set("name");
        paramThreadLocal.set(new HashMap(){
            {
                put("id","1");
                put("name","xiaoTOT");
                put("gender","M");
            }
        });
        testParam();
    }

    private void testParam() {
        //從線程本地變量獲取參數
        Map map = paramThreadLocal.get();
        map.forEach((key,value)->{
            System.out.println("key:"+key+" & value="+value);
        });
    }

ThreadLocal改造改造在單例模式中的運用

單例模式的好處無用質疑,能夠減小對象的建立。對於那些建立很是費時的對象尤爲明顯。而且若是可以用單例解決的問題經歷使用單例解決,這樣能減輕運行時的壓力。

  1. 對於一個對象假若沒有成員變量,單例很是簡單,不用去擔憂多線程同時對成員變量修改而產生的線程安全問題。

  2. 對於一個擁有成員變量的對象使用單例就須要考慮到線程安全問題。多線程訪問又能夠分爲下面兩個方面:

a:成員變量須要多線程同步,好比帳戶對象(ACCOUNT)中的成員變量餘額(amount).amount成員變量須要在多線程的訪問下保證各個線程保證絕對的同步,即不管何時線程內的值都是同樣。咱們能夠經過加同步關鍵字synchronized,volatile來解決。

b,成員變量不須要線程同步,每一個線程訪問本身線程內部的對象。好比一個服務類對數據庫的連接成員變量,每一個線程分配一個鏈接便可。相似這種場景,咱們最簡單的方式是使用多例模式來解決。單更好的方式是經過threadLocal來解決。

下面是使用ThreadLocal改造單例模式的示例:

//ThreadLocal在單例模式改造的運用
@Test
public  void test03(){
    //單例模式的好處無用質疑,能夠減小對象的建立。對於那些建立很是費時的對象尤爲明顯。而且若是可以用單例解決的問題經歷使用單例解決,這樣能減輕運行時的壓力。
    //1,對於一個對象假若沒有成員變量,單例很是簡單,不用去擔憂多線程同時對成員變量修改而產生的線程安全問題。
    //2,對於一個擁有成員變量的對象使用單例就須要考慮到線程安全問題。多線程訪問又能夠分爲下面兩個方面:
    // a:成員變量須要多線程同步,好比帳戶對象(ACCOUNT)中的成員變量餘額(amount).amount成員變量須要在多線程的訪問下保證各個線程保證絕對的同步,即不管何時線程內的值都是同樣。
    // 咱們能夠經過加同步關鍵字synchronized,volatile來解決。
    // b,成員變量不須要線程同步,每一個線程訪問本身線程內部的對象。好比一個服務類對數據庫的連接成員變量,每一個線程分配一個鏈接便可。相似這種場景,咱們最簡單的方式是使用多例模式來解決。
    //單更好的方式是經過threadLocal來解決。

    //多例模式
    Thread thread = new Thread(){
        @Override
        public void run() {

            DBConnect dbConnect = new DBConnect();
            DemoService demoService = new DemoService();
            demoService.setConnect(dbConnect);
            demoService.doSomeThing();

        }
    };

    Thread thread2 = new Thread(){
        @Override
        public void run() {

            DBConnect dbConnect = new DBConnect();
            DemoService demoService = new DemoService();
            demoService.setConnect(dbConnect);
            demoService.doSomeThing();

        }
    };

    thread.start();
    thread2.start();

    // 單例模式改造
    // 由DemoService構造器能夠看出,構造這個對象是很是耗時的。而且還不能使用單例模式,由於DBConnect是不能多線程訪問的。遇到這種狀況那就使用ThreadLocal來改造吧。
    //若是能修改DemoService源碼,修改源碼便可。若不能修該源碼(好比DemoService是一個三方包)單DemoService不是final的,便可以經過繼承修改。

    DemoService demoService1 = new ThreadLocalDemoService();

    Thread threadA = new Thread(){
        @Override
        public void run() {

            demoService1.setConnect(new DBConnect());
            demoService1.doSomeThing();

        }
    };

    Thread threadB = new Thread(){
        @Override
        public void run() {

            demoService1.setConnect(new DBConnect());
            demoService1.doSomeThing();

        }
    };

    threadA.start();
    threadB.start();


}

static class DemoService{

    //這個對象不能線程同時訪問,應該是一個線程就創建一個鏈接到數據庫。不一樣的線程不能使用同一個鏈接。
    DBConnect connect;

    public DemoService(){
        try {
            Thread.sleep(5l);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
    public void setConnect(DBConnect connect){
        this.connect = connect;
    }


    public void doSomeThing(){
        connect.updateSomeData();
    }
}

//使用ThreadLocal改形成員變量,使其可使其可使用單例模式
static class ThreadLocalDemoService extends DemoService {

    ThreadLocal<DBConnect> connectThreadLocal = new ThreadLocal<>();

    public ThreadLocalDemoService() {
        super();
    }



    public void doSomeThing(){
        connectThreadLocal.get().updateSomeData();
    }

    public void setConnect(DBConnect dbConnect){
        connectThreadLocal.set(dbConnect);
    }

}



class DBConnect {
    private String transactionName = Thread.currentThread().getName()+"的事務";

    public void updateSomeData(){
        System.out.println(transactionName + " update some data");
    }

}

其中DemoService中有個成語變量DBConnect connect,因爲資源不能同時被連個線程使用,好比socket連接發送數據,或者數據庫事務,一個線程不能影響另一個線程的事務。 這個時候咱們沒有辦法只有對DemoService採用多例模式,單由由於DemoService建立會耗費大量時間。相似的例子不少,好比一個對象中,可能只有少數成員變量不可以多線程訪問,大多數是能多線程訪問的,這時爲了一個成員變量去將單例改爲多例也是很是糟糕的。這時若咱們使用ThreadLocal就可以完美解決。

備註

值得注意的是,ThreadLocal隨着當前線程的銷燬而銷燬,若是程序中採用線程池,在上一次任務運行完以後,記得清掉以前ThreadLocal數據。

引用

實際上學習和使用ThreadLocal以前,也百多過不少ThreadLocal相關的文章。最開始是拜讀了學習Spring必學的Java基礎知識(6)----ThreadLocal@ITEYE這篇文章,才瞭解到ThreadLocal這個東西。最好爲了詳細瞭解,看到了第二篇文章,而且以前看過的關於ThreadLocal的文章與這篇文章內容基本上都同樣,都在講關於Threadloal爲解決線程安全問題提供了新思路,當時被看得一頭霧水。最好看到第三篇的帖子。而後結合源代碼纔對ThreadLocal的本質很好的瞭解。若是你看完本篇文章還不是很是明白,能夠詳細參閱第三篇引用,這個帖子的討論仍是很是精彩的,能給你不少啓迪做用。須要說明的是第二篇CSDN相關講解其實是有問題的。你能夠看看文章下方的評論。

  1. 學習Spring必學的Java基礎知識(6)----ThreadLocal@ITEYE

  2. 完全理解ThreadLocal@CSDN

  3. 正確理解ThreadLocal@ITEYE

相關文章
相關標籤/搜索