JAVA併發編程(六):線程本地變量ThreadLocal與TransmittableThreadLocal

volatile_logo

我們知道有時候一個對象的共享變量會被多個線程所訪問,這時就會有線程安全問題。當然我們可以使用synchorinized 關鍵字來爲此變量加鎖,進行同步處理。從而限制只能有一個線程來使用此變量,但是加鎖會大大影響程序執行效率,此外我們還可以使用ThreadLocal來解決對某一個變量的訪問衝突問題。

一、ThreadLocal 概述

當使用ThreadLocal維護變量的時候 它爲每一個使用該變量的線程提供一個獨立的變量副本,即每個線程內部都會有一個該變量,這樣同時多個線程訪問該變量並不會彼此相互影響,因此他們使用的都是自己從內存中拷貝過來的變量的副本, 這樣就不存在線程安全問題,也不會影響程序的執行性能。
ThreadLocal 的幾個方法: ThreadLocal 可以存儲任何類型的變量對象, get返回的是一個Object對象,但是我們可以通過泛型來制定存儲對象的類型。

public T get() { } // 用來獲取ThreadLocal在當前線程中保存的變量副本
public void set(T value) { } //set()用來設置當前線程中變量的副本
public void remove() { } //remove()用來移除當前線程中變量的副本
protected T initialValue() { } //initialValue()是一個protected方法,一般是用來在使用時進行重寫的

Thread 在內部是通過ThreadLocalMap來維護ThreadLocal變量表, 在Thread類中有一個threadLocals 變量,是ThreadLocalMap類型的,它就是爲每一個線程來存儲自身的ThreadLocal變量的, ThreadLocalMap是ThreadLocal類的一個內部類,這個Map裏面的最小的存儲單位是一個Entry, 它使用ThreadLocal作爲key, 變量作爲 value,這是因爲在每一個線程裏面,可能存在着多個ThreadLocal變量

初始時,在Thread裏面,threadLocals爲空,當通過ThreadLocal變量調用get()方法或者set()方法,就會對Thread類中的threadLocals進行初始化,並且以當前ThreadLocal變量爲鍵值,以ThreadLocal要保存的副本變量爲value,存到threadLocals。
然後在當前線程裏面,如果要使用副本變量,就可以通過get方法在threadLocals裏面查找
我們來看一個使用示例:

public class Test {
    ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
    ThreadLocal<String> stringLocal = new ThreadLocal<String>();


    public void set() {
        longLocal.set(Thread.currentThread().getId());
        stringLocal.set(Thread.currentThread().getName());
    }

    public long getLong() {
        return longLocal.get();
    }

    public String getString() {
        return stringLocal.get();
    }

    public static void main(String[] args) throws InterruptedException {
        final Test test = new Test();


        test.set();
        System.out.println(test.getLong());
        System.out.println(test.getString());


        Thread thread1 = new Thread(){
            public void run() {
                test.set();
                System.out.println(test.getLong());
                System.out.println(test.getString());
            };
        };
        thread1.start();
        thread1.join();

        System.out.println(test.getLong());
        System.out.println(test.getString());
    }
}

輸出結果爲

1 
main 
9 
Thread-0 
1 
main

二、父子線程傳遞InheritableThreadLocal

以上方案在父子線程中就有了侷限性,如果子線程想要拿到父線程的中的ThreadLocal值怎麼辦呢?比如會有以下的這種代碼的實現。由於ThreadLocal的實現機制,在子線程中get時,我們拿到的Thread對象是當前子線程對象,那麼他的ThreadLocalMap是null的,所以我們得到的value也是null。

final ThreadLocal threadLocal=new ThreadLocal(){
            @Override
            protected Object initialValue() {
                return "xiezhaodong";
            }
        };
 new Thread(new Runnable() {
            @Override
            public void run() {
                threadLocal.get();//NULL
            }
        }).start();

JDK已經爲這種情況提供了實現方案:InheritableThreadLocal。大致的解釋了一下InheritableThreadLocal爲什麼能解決父子線程傳遞Threadlcoal值的問題。
1)在創建InheritableThreadLocal對象的時候賦值給線程的t.inheritableThreadLocals變量
2)在創建新線程的時候會check父線程中t.inheritableThreadLocals變量是否爲null,如果不爲null則copy一份ThradLocalMap到子線程的t.inheritableThreadLocals成員變量中去
3)因爲複寫了getMap(Thread)和CreateMap()方法,所以get值得時候,就可以在getMap(t)的時候就會從t.inheritableThreadLocals中拿到map對象,從而實現了可以拿到父線程ThreadLocal中的值

所以,在最開始的代碼示例中,如果把ThreadLocal對象換成InheritableThreadLocal對象,那麼get到的字符會是“xiezhaodong”而不是NULL

二、線程池傳遞TransmittableThreadLocal

我們在使用線程的時候往往不會只是簡單的new Thrad對象,而是使用線程池,當然線程池的好處多多。這裏不詳解,既然這裏提出了問題,那麼線程池會給InheritableThreadLocal帶來什麼問題呢?我們列舉一下線程池的特點:

1)爲了減小創建線程的開銷,線程池會緩存已經使用過的線程
2)生命週期統一管理,合理的分配系統資源

對於第一點,如果一個子線程已經使用過,並且會set新的值到ThreadLocal中,那麼第二個task提交進來的時候還能獲得父線程中的值嗎?答案是不能,如果我們能夠,在使用完這個線程的時候清除所有的localMap,在submit新任務的時候在重新重父線程中copy所有的Entry。然後重新給當前線程的t.inhertableThreadLocal賦值。這樣就能夠解決在線程池中每一個新的任務都能夠獲得父線程中ThreadLocal中的值而不受其他任務的影響,因爲在生命週期完成的時候會自動clear所有的數據。Alibaba的一個庫解決了這個問題github:alibaba/transmittable-thread-local
如何使用
這個庫最簡單的方式是這樣使用的,通過簡單的修飾,使得提交的runable擁有了上一節所述的功能。具體的API文檔詳見github,這裏不再贅述

TransmittableThreadLocal<String> parent = new TransmittableThreadLocal<String>();
parent.set("value-set-in-parent");

Runnable task = new Task("1");
// 額外的處理,生成修飾了的對象ttlRunnable
Runnable ttlRunnable = TtlRunnable.get(task); 
executorService.submit(ttlRunnable);

// Task中可以讀取, 值是"value-set-in-parent"
String value = parent.get();

原理簡述
這個方法TtlRunnable.get(task)最終會調用構造方法,返回的是該類本身,也是一個Runable,這樣就完成了簡單的裝飾。最重要的是在run方法這個地方。

public final class TtlRunnable implements Runnable {
    private final AtomicReference<Map<TransmittableThreadLocal<?>, Object>> copiedRef;
    private final Runnable runnable;
    private final boolean releaseTtlValueReferenceAfterRun;

    private TtlRunnable(Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
    //從父類copy值到本類當中
        this.copiedRef = new AtomicReference<Map<TransmittableThreadLocal<?>, Object>>(TransmittableThreadLocal.copy());
        this.runnable = runnable;//提交的runable,被修飾對象
        this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
    }
    /**
     * wrap method {@link Runnable#run()}.
     */
    @Override
    public void run() {
        Map<TransmittableThreadLocal<?>, Object> copied = copiedRef.get();
        if (copied == null || releaseTtlValueReferenceAfterRun && !copiedRef.compareAndSet(copied, null)) {
            throw new IllegalStateException("TTL value reference is released after run!");
        }
        //裝載到當前線程
        Map<TransmittableThreadLocal<?>, Object> backup = TransmittableThreadLocal.backupAndSetToCopied(copied);
        try {
            runnable.run();//執行提交的task
        } finally {
        //clear
            TransmittableThreadLocal.restoreBackup(backup);
        }
    }
}

在上面的使用線程池的例子當中,如果換成這種修飾的方式進行操作,B任務得到的肯定是父線程中ThreadLocal的值,解決了在線程池中InheritableThreadLocal不能解決的問題。
如何更新父線程ThreadLocal值?
如果線程之間出了要能夠得到父線程中的值,同時想更新值怎麼辦呢?在前面我們有提到,當子線程copy父線程的ThreadLocalMap的時候是淺拷貝的,代表子線程Entry裏面的value都是指向的同一個引用,我們只要修改這個引用的同時就能夠修改父線程當中的值了,比如這樣:

@Override
          public void run() {
              System.out.println("========");
              Span span=  inheritableThreadLocal.get();
              System.out.println(span);
              span.name="liuliuliu";//修改父引用爲liuliuliu
              inheritableThreadLocal.set(new Span("zhangzhangzhang"));
              System.out.println(inheritableThreadLocal.get());
          }

這樣父線程中的值就會得到更新了。能夠滿足父線程ThreadLocal值的實時更新,同時子線程也能共享父線程的值。不過場景倒是不是很常見的樣子。

參考文章


本文作者: catalinaLi
本文鏈接: http://catalinali.top/2018/helloThreadLocal/

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