天天看點

java線程變量那點事兒

文章目錄

      • 寫在前面
      • 1. java線程
        • 1.1 線程開啟
        • 1.2 線程内部主要參數
      • 2. ThreadLocal
        • 2.1 ThreadLocal是什麼
        • 2.2 ThreadLocal都應用于哪些場景
        • 2.3 ThreadLocal是如何實作擷取到設定的變量的呢?
        • 2.4 ThreadLocal缺陷
      • 3. InheritableThreadLocal
        • 3.1 InheritableThreadLocal的作用
        • 3.2 InheritableThreadLocal的缺陷
      • 4. TransmittableThreadLocal
        • 4.1 TransmittableThreadLocal的作用
        • 4.2 TransmittableThreadLocal提供的無侵入式實作
      • 5. 舉例:hystrix線上程隔離模式下的上下文變量擷取
        • 說明
        • 在使用SleuthHystrixConcurrencyStrategy接入的例子
      • 寫在後面
      • 附錄
        • 線程狀态說明

寫在前面

該文章主要介紹ThreadLocal以及其變種,是以線程的方法、線程池、以及涉及線程方法的一些變量不在該文章講解;

後續可能會陸續出線程方法詳解、同步塊、線程池内部詳解等;(如果有時間的話)

因為ThreadLocal的講解可能會涉及一些線程池等内容,但不會用過多篇幅進行講解。

1. java線程

1.1 線程開啟

在java中線程是無處不在的;那麼如何才能開啟線程呢?
  • 自己的類繼承Thread,重寫run方法,或者直接new Thread;然後調用其start方法即可
  • 自己的類實作Runnable接口,實作其run方法,new Thread(runnable).start()即可;線程池内部也是啟動Thread;java8及以上版本可以使用lambda來寫run方法中的實作,如new Thread(() -> System.out.println(“測試”)).start();

1.2 線程内部主要參數

變量名 類型 主要作用 備注
name String 辨別線程名稱 名稱随時可以修改volatile修飾
priority int 優先級 1-10之間,不能保證絕對優先
daemon boolean 守護線程标志 預設為非守護線程
target Runnable 運作目标 如果target==null,那麼運作自身run方法
group ThreadGroup 所屬線程組 便于管理相同或類似任務的線程
contextClassLoader ClassLoader 上下文類加載器 可随時設定于修改
threadLocals ThreadLocalMap 上下文變量 時常用于上下文參數設定和擷取
inheritableThreadLocals ThreadLocalMap 上下文變量 時常用于父子線程參數設定和擷取
tid long 線程唯一辨別
threadStatus int 線程狀态 線程狀态說明

2. ThreadLocal

2.1 ThreadLocal是什麼

ThreadLocal是用于存儲線程在任務執行過程便于擷取參數和設定參數的線程變量,它是跟線程緊密相關的;它的出現是為了解決多線程環境下的變量通路并發問題;是以單線程的任務或程序根本用不着它,直接用全局執行個體變量或類變量即可。

2.2 ThreadLocal都應用于哪些場景

  • 用過struts2的人或多或少都接觸過這麼一個類ServletActionContext;

    它繼承至ActionContext;從ServletActionContext可以很友善的拿到request

    等變量;有沒有想過它是怎麼辦到的呢,如果同時有10個使用者發起同一個或 者不同的請求,它是如何精确的告知你這次請求的request是什麼呢?

    其核心功臣就是ThreadLocal啦!

  • spring中事務管理中的TransactionSynchronizationManager中充斥着ThreadLocal,用于處理其接管的事務;
  • 自定義ThreadLocal,假設項目中有個filter,該filter是用于解碼資訊的,

    那麼解碼後的資訊如何傳遞下去呢,當然還是使用ThreadLocal啦…

  • 說了這麼多,來時應該來個示範代碼了

lombok依賴

<dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.18</version>
            <scope>provided</scope>
        </dependency>
           

上下文管理類

package com.littlehow.context;
import com.littlehow.model.User;

/**
 * 基于線程上下文的使用者資訊管理
 */
public class UserContext {
    private static final ThreadLocal<User> context = new ThreadLocal<>();

    /**
     * 設定使用者資訊
     * @param user  -- 使用者資訊
     */
    public static void set(User user) {
        context.set(user);
    }

    /**
     * 擷取使用者資訊
     * @return -- 使用者資訊
     */
    public static User get() {
        return context.get();
    }

    /**
     * 移除使用者資訊
     */
    public static void remove() {
        context.remove();
    }
}
           

使用者類

package com.littlehow.model;
import lombok.Data;
import lombok.ToString;
import java.time.LocalDate;

@Data
@ToString
public class User {
    private Integer userId;
    private String name;
    private LocalDate birthday;
}
           

使用者服務類

package com.littlehow.biz;
import com.littlehow.context.UserContext;
public class UserService {
    /**
     * 執行添加使用者
     */
    public void addUser() {
        System.out.println(Thread.currentThread().getName() + "添加使用者資訊:" + UserContext.get());
    }
}

           

測試調用類

package com.littlehow.biz;
import com.littlehow.context.UserContext;
import com.littlehow.model.User;
import java.time.LocalDate;
import java.util.concurrent.atomic.AtomicInteger;

public class CallService {
    //使用者編号建立器
    private static final AtomicInteger creator = new AtomicInteger(1);
    //備選生日
    private static final LocalDate[] birthdays = {LocalDate.of(1988, 9, 11),
            LocalDate.of(1989, 11, 10),
            LocalDate.of(1990, 3, 7),
            LocalDate.of(1995, 7, 26),
            LocalDate.of(2000, 10, 1)
    };
    private static final int birthdayLength = birthdays.length;

    public static void main(String[] args) {
        UserService userService = new UserService();
        //同時10個調用
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                UserContext.set(initUser(Thread.currentThread().getName()));
                //進行調用
                userService.addUser();
            }, "callService-" + i).start();
        }
    }

    /**
     * 初始化使用者資訊(模拟請求)
     * @param name  -- 使用者名
     * @return  -- 使用者資訊
     */
    private static User initUser(String name) {
        User user = new User();
        user.setUserId(creator.getAndIncrement());
        user.setName(name);
        user.setBirthday(birthdays[user.getUserId() % birthdayLength]);
        return user;
    }
}
           

測試模拟10個使用者添加的請求,使用者資訊放置于上下文中,目标是線程設定的上下文将由相同線程來擷取并處理;

以上CallService運作結果如下(得到預期結果):

callService-0添加使用者資訊:User(userId=1, name=callService-0, birthday=1989-11-10)

callService-2添加使用者資訊:User(userId=3, name=callService-2, birthday=1995-07-26)

callService-3添加使用者資訊:User(userId=4, name=callService-3, birthday=2000-10-01)

callService-1添加使用者資訊:User(userId=2, name=callService-1, birthday=1990-03-07)

callService-4添加使用者資訊:User(userId=5, name=callService-4, birthday=1988-09-11)

callService-5添加使用者資訊:User(userId=6, name=callService-5, birthday=1989-11-10)

callService-9添加使用者資訊:User(userId=7, name=callService-9, birthday=1990-03-07)

callService-6添加使用者資訊:User(userId=8, name=callService-6, birthday=1995-07-26)

callService-7添加使用者資訊:User(userId=9, name=callService-7, birthday=2000-10-01)

callService-8添加使用者資訊:User(userId=10, name=callService-8, birthday=1988-09-11)

2.3 ThreadLocal是如何實作擷取到設定的變量的呢?

這時候上面提到的線程變量threadLocals發揮作用的時候,我們來仔細看看,看完之後就能明确知道其為何為如此了,ThreadLocal變量的設定和擷取代碼:

public void set(T value) {
		//擷取目前線程
        Thread t = Thread.currentThread();
        //擷取存放線程變量的map
        //getMap(t)在ThreadLocal中的實作為:return t.threadLocals;
        //是目前線程的變量,如果map存在則在該map下繼續設定
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
           //如果不存在則為目前線程的線程變量指派   
           //createMap在ThreadLocal下的實作:
           //t.threadLocals = new ThreadLocalMap(this, firstValue); 
           //由此可以看出,不管在外部設定初始化了多少個ThreadLocal,其實線上程中
           //都隻有一個Map變量,map的key就是ThreadLocal執行個體,是以從ThreadLocal讀
           //取出來的資料都是自己想要的。
            createMap(t, value);
    }

public T get() {
        //同set
        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;
            }
        }
        //如果map不存在,設定一個初始值null方法map,如果map不存在,則初始化
        //map再放入目前線程變量,最後傳回該初始的null值
        return setInitialValue();
    }
           

2.4 ThreadLocal缺陷

其實ThreadLocal還是存在很大限制的,它僅僅能擷取自己目前線程設定的變量,那麼有些功能比較複雜或者調用比較多的時候,可能會在調用過程中繼續開啟線程,那麼在新開啟的線程中就擷取不到結果了,但這往往不是我們想要的;下面是示範代碼(将callService稍稍做一點改動):

public static void main(String[] args) {
			//main作為目前調用線程
        	UserContext.set(initUser(Thread.currentThread().getName()));
        	UserService userService = new UserService();
        	//開啟新線程來進行調用
        	new Thread(() -> userService.addUser(), "callService").start();
       }
           

本來期望是能出現callService添加使用者資訊:User(userId=1, name=main, birthday=1989-11-10)

結果運作後輸出:callService添加使用者資訊:null

最初始設定的User資訊不見了;就好比有個真實項目,裡面有個loginFilter,可以使用者攔截未登入使用者并且将已登入使用者的使用者資訊放置于線程上下文中,然後業務處理的時候開啟新的線程,在需要擷取使用者資訊的時候就會出現擷取不到的情況。

3. InheritableThreadLocal

3.1 InheritableThreadLocal的作用

上一節說到ThreadLocal遇到線程中開啟線程後,就不能擷取到初始線程設定的變量值了,為了解決這個問題,InheritableThreadLocal應運而生,它繼承至ThreadLocal,它線上程中的線程變量是inheritableThreadLocals;

為什麼InheritableThreadLocal就能解決上面的問題呢,我們先将之前代碼改造運作一次再進入其源碼進行分析:

将UserContext裡面的上下文變量申明改為下面的代碼即可:

private static final ThreadLocal<User> context = new InheritableThreadLocal<>();
           

繼續運作上面的代碼後得到結果(符合預期):

callService添加使用者資訊:User(userId=1, name=main, birthday=1989-11-10)

下面就來看看源碼是如何實作的

InheritableThreadLocal

ThreadLocalMap getMap(Thread t) {
	  // 僅僅是将擷取map變為從線程中擷取inheritableThreadLocals變量
       return t.inheritableThreadLocals;
    }
   
    void createMap(Thread t, T firstValue) {
    	//僅僅是将指派改為設定到線程的inheritableThreadLocals 變量
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
           
從InheritableThreadLocal中的代碼來看,其實不可能實作線程中傳遞的,因為它僅僅重寫了上面兩個方法,那麼為什麼運作結果符合預期呢,那就隻能從線程自己入手了。

Thread的部分方法:

//我測試代碼中使用的線程的構造方法
	public Thread(Runnable target, String name) {
	    //核心是init方法,其他構造方法也是調用init方法進行線程的初始化
        init(null, target, name, 0);
    }

   
	private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {//由此可見name是必須設定,預設是thread-内部維護的自增方法
                          //此處就不發散開了
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;
		//将目前線程設定為新線程的父線程
        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            /* Determine if it's an applet or not */

            /* If there is a security manager, ask the security manager
               what to do. */
            if (security != null) {
                g = security.getThreadGroup();
            }

            /* If the security doesn't have a strong opinion of the matter
               use the parent thread group. */
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }

        /* checkAccess regardless of whether or not threadgroup is
           explicitly passed in. */
        g.checkAccess();

        /*
         * Do we have the required permissions?
         */
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }

        g.addUnstarted();
        this.group = g;
        //初始時沿用父線程的守護線程屬性
        this.daemon = parent.isDaemon();
        //初始時沿用父線程的優先級
        this.priority = parent.getPriority();
        //上下文類加載器的設定,這個可以寫一個關于類加載器的文章來具體介紹,此處
        //就不發散了
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        //構造方法中傳入了Runnable接口就有,否則為null,為null則調用在即的run方法        
        this.target = target;
        setPriority(priority);
        //#####此處是關鍵之處#######
        //預設inheritThreadLocals =true,那麼就關心父線程的inheritableThreadLocals 變量了
        //由InheritableThreadLocal重寫的兩個方法可以看出,父線程如果使用其設定了上下文變量
        //那麼parent.inheritableThreadLocals是有值得
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
           // 将父線程的變量周遊到子線程的inheritableThreadLocals 變量中
           //進而實作了新開線程也能擷取到父線程設定的變量值了,
           //而且從該方法可以看出,線程的兒子可以得到,線程的孫子也能通過同樣的方法擷取到
           //該過程是自上而下傳遞的
           //#####此處是關鍵之處########
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }
           

3.2 InheritableThreadLocal的缺陷

是否覺得使用InheritableThreadLocal來傳遞上下文變量就可以一勞永逸了呢?

要是問題都這麼簡單就好了,但往往事與願違啊!

到這裡就不得不提一嘴線程池了,因為線程的建立和銷毀是很耗費資源的一件事情,那麼在高性能高并發的場景下如果頻繁的建立線程銷毀線程明顯是不可取的,是以java前輩們自然而然就想到了線程的複用啦!線程池就是線程複用模型的一個實作并廣泛運用于各個場景。因為線程池不是本文主要介紹對象,是以就不具體介紹其實作核心和原理了。

線程池為什麼會是InheritableThreadLocal的缺陷呢,先來改造一下代碼看看效果再說啦!

将CallService類改造如下:

package com.littlehow.biz;

import com.littlehow.context.UserContext;
import com.littlehow.model.User;

import java.time.LocalDate;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

public class CallService {
    //使用者編号建立器
    private static final AtomicInteger creator = new AtomicInteger(1);
    //備選生日
    private static final LocalDate[] birthdays = {LocalDate.of(1988, 9, 11),
            LocalDate.of(1989, 11, 10),
            LocalDate.of(1990, 3, 7),
            LocalDate.of(1995, 7, 26),
            LocalDate.of(2000, 10, 1)
    };
    private static final int birthdayLength = birthdays.length;

    //申明一個簡單的線程池,3個核心線程
    private static final AtomicInteger threadIdCreator = new AtomicInteger(1);
    private static ExecutorService pool = Executors.newFixedThreadPool(3, (runnable) ->
         new Thread(runnable, "littlehow-" + threadIdCreator.getAndIncrement())
    );

    public static void main(String[] args) {
        UserService userService = new UserService();
        //同時10個調用
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                UserContext.set(initUser(Thread.currentThread().getName()));
                //使用線程池進行調用
                pool.execute(userService::addUser);
            }, "callService-" + i).start();
        }
    }

    /**
     * 初始化使用者資訊(模拟請求)
     * @param name  -- 使用者名
     * @return  -- 使用者資訊
     */
    private static User initUser(String name) {
        User user = new User();
        user.setUserId(creator.getAndIncrement());
        user.setName(name);
        user.setBirthday(birthdays[user.getUserId() % birthdayLength]);
        return user;
    }
}

           

運作CallService得到以下結果:

littlehow-1添加使用者資訊:User(userId=2, name=callService-1, birthday=1990-03-07)

littlehow-2添加使用者資訊:User(userId=6, name=callService-5, birthday=1989-11-10)

littlehow-3添加使用者資訊:User(userId=3, name=callService-2, birthday=1995-07-26)

littlehow-1添加使用者資訊:User(userId=2, name=callService-1, birthday=1990-03-07)

littlehow-3添加使用者資訊:User(userId=3, name=callService-2, birthday=1995-07-26)

littlehow-2添加使用者資訊:User(userId=6, name=callService-5, birthday=1989-11-10)

littlehow-3添加使用者資訊:User(userId=3, name=callService-2, birthday=1995-07-26)

littlehow-1添加使用者資訊:User(userId=2, name=callService-1, birthday=1990-03-07)

littlehow-3添加使用者資訊:User(userId=3, name=callService-2, birthday=1995-07-26)

littlehow-2添加使用者資訊:User(userId=6, name=callService-5, birthday=1989-11-10)

從輸出可以看出,線程池中的線程執行了10次,但是輸出的User資訊來來回回就隻有3個,與預期的userId[1-10]有很大差距;造成這種現象是因為線程複用導緻的;

      從之前的分析可以看出InheritableThreadLocal類型的變量,隻有線上程初始化的時候才會被指派,因為使用的線程池,導緻當開啟的線程數=核心線程數時,将不在新開啟線程,而是使用之前的線程來進行目前的任務,那麼目前任務擷取的上下文變量肯定就是第一次初始化線程時設定進去的。

     這個現象明顯不是我們期望的,因為造成資料讀取錯亂,并且越往後越不可能擷取到正确的資料資訊。

4. TransmittableThreadLocal

4.1 TransmittableThreadLocal的作用

上一節最後說到了InheritableThreadLocal線上程池中造成的資料混亂,那是否就無解了呢?

TransmittableThreadLocal就應運而生了,它的出現就是為了解決在使用線程池時資料讀取混亂的問題,而且它也繼承至InheritableThreadLocal,還是先來改造下代碼,然後逐漸分析…

目前的TransmittableThreadLocal maven最新版本為9月更新的2.8.1

<dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>transmittable-thread-local</artifactId>
      <version>2.8.1</version>
 </dependency>
           

首先将UserContext裡面的上下文變量申明改為下面的代碼:

private static final ThreadLocal<User> context = new TransmittableThreadLocal<>();
           

其次再把剛剛方法線程池執行的runnable申明該為下面的代碼:

pool.execute(TtlRunnable.get(userService::addUser));
           

調用CallService輸出如下:

littlehow-1添加使用者資訊:User(userId=2, name=callService-1, birthday=1990-03-07)

littlehow-1添加使用者資訊:User(userId=7, name=callService-7, birthday=1990-03-07)

littlehow-1添加使用者資訊:User(userId=10, name=callService-9, birthday=1988-09-11)

littlehow-3添加使用者資訊:User(userId=1, name=callService-0, birthday=1989-11-10)

littlehow-1添加使用者資訊:User(userId=5, name=callService-5, birthday=1988-09-11)

littlehow-3添加使用者資訊:User(userId=6, name=callService-4, birthday=1989-11-10)

littlehow-1添加使用者資訊:User(userId=8, name=callService-8, birthday=1995-07-26)

littlehow-3添加使用者資訊:User(userId=3, name=callService-2, birthday=1995-07-26)

littlehow-1添加使用者資訊:User(userId=9, name=callService-6, birthday=2000-10-01)

littlehow-2添加使用者資訊:User(userId=4, name=callService-3, birthday=2000-10-01)

由以上輸出可以看出,10個任務都被正确執行,也就是說都擷取了正确的參數了,那麼TransmittableThreadLocal到底是如何做到的呢?下面還是來進行簡單的源碼分析:

首先肯定是先看看TransmittableThreadLocal類的主要實作啦:

//它并沒有像InheritableThreadLocal那樣重寫getMap方法,而是重寫get,set,remove
    @Override
    public final T get() {
        T value = super.get();
        if (null != value) {
           // 關鍵之處在于此
            addValue();
        }
        return value;
    }

    @Override
    public final void set(T value) {
        super.set(value);
        if (null == value) { // may set null to remove value
            //關鍵之處,這裡如果設定為null,如果上一個任務執行留下了資料,那麼必須移除
            removeValue();
        } else {
           //關鍵之處
            addValue();
        }
    }

    @Override
    public final void remove() {
       // 關鍵之處
        removeValue();
        super.remove();
    }
           

核心之處就在于它在原先設定、擷取、删除值得地方都加上一自己的方法,

具體如下:

// Note about holder:
    // 1. The value of holder is type Map<TransmittableThreadLocal<?>, ?> (WeakHashMap implementation),
    //    but it is used as *set*.
    // 2. WeakHashMap support null value.
    //此處有一個InheritableThreadLocal用于緩存父線程設定的線程變量,以線程的ThreadLocal作為Key
    private static InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>> holder =
            new InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>>() {
                @Override
                protected Map<TransmittableThreadLocal<?>, ?> initialValue() {
                    return new WeakHashMap<TransmittableThreadLocal<?>, Object>();
                }

                @Override
                protected Map<TransmittableThreadLocal<?>, ?> childValue(Map<TransmittableThreadLocal<?>, ?> parentValue) {
                    return new WeakHashMap<TransmittableThreadLocal<?>, Object>(parentValue);
                }
            };
    //剛剛提到過的關鍵之處,在set和get的時候會用到的方法
    private void addValue() {
       //首先判斷在此線程是否已經設定過了ThreadLocal,沒有設定就緩存起來
        if (!holder.get().containsKey(this)) {
            holder.get().put(this, null); // WeakHashMap supports null value.
        }
    }
    //移除緩存的資訊
    private void removeValue() {
        holder.get().remove(this);
    }

	//此處才是真正實作了參數傳遞的第一部分
    public static Object capture() {
            Map<TransmittableThreadLocal<?>, Object> captured = new HashMap<TransmittableThreadLocal<?>, Object>();
            for (TransmittableThreadLocal<?> threadLocal : holder.get().keySet()) {
                captured.put(threadLocal, threadLocal.copyValue());
            }
            return captured;
        }

//重新設定線程變量到線程池中的線程變量中
	public static Object replay(@Nonnull Object captured) {
            @SuppressWarnings("unchecked")
            Map<TransmittableThreadLocal<?>, Object> capturedMap = (Map<TransmittableThreadLocal<?>, Object>) captured;
            Map<TransmittableThreadLocal<?>, Object> backup = new HashMap<TransmittableThreadLocal<?>, Object>();

            for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator();
                 iterator.hasNext(); ) {
                Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next();
                TransmittableThreadLocal<?> threadLocal = next.getKey();

                // backup
                backup.put(threadLocal, threadLocal.get());

                // clear the TTL values that is not in captured
                // avoid the extra TTL values after replay when run task
                if (!capturedMap.containsKey(threadLocal)) {
                    iterator.remove();
                    threadLocal.superRemove();
                }
            }

            // set values to captured TTL
            setTtlValuesTo(capturedMap);

            // call beforeExecute callback
            doExecuteCallback(true);

            return backup;
        }

	private static void setTtlValuesTo(@Nonnull Map<TransmittableThreadLocal<?>, Object> ttlValues) {
            for (Map.Entry<TransmittableThreadLocal<?>, Object> entry : ttlValues.entrySet()) {
                @SuppressWarnings("unchecked")
                TransmittableThreadLocal<Object> threadLocal = (TransmittableThreadLocal<Object>) entry.getKey();
                //将值設定到線程池給出的執行線程中,運作run的時候自然就能取到。
                threadLocal.set(entry.getValue());
            }
        }
           

當然線程的執行才是真正運用前面設定資訊的地方TtlRunnable實作了Runnable接口,并且為final類:

private TtlRunnable(@Nonnull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
        //這裡的capture就是上面代碼中的capture方法,作用是将從父線程那裡設定
        //的線程變量捕獲到此處
        this.capturedRef = new AtomicReference<Object>(capture());
        this.runnable = runnable;
        this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
    }

@Override
    public void run() {
        Object captured = capturedRef.get();
        if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
            throw new IllegalStateException("TTL value reference is released after run!");
        }
         //複制并設定線程變量值,并且備份線程執行之前的變量值
        Object backup = replay(captured);
        try {
           //執行任務,這裡就會去到新設定進去的值
            runnable.run();
        } finally {
           // 恢複之前設定的變量值,這樣可以保證不破壞原有線程變量資料
            restore(backup);
        }
    }
           

到這裡就解釋了為什麼使用TransmittableThreadLocal能擷取到線程變量了,當然它必須配合如TtlRunnable/TtlCallable等一起使用,也可以配合ExecutorServiceTtlWrapper的線程池使用,其實就是它内部幫你封裝Runnable轉TtlRunnable這樣的工序而已。

4.2 TransmittableThreadLocal提供的無侵入式實作

上面的代碼不是需要改變線程的實作就是要改變線程池的實作,如果原有代碼已經封裝完畢該如何處理呢?

TransmittableThreadLocal提供了JVM級别的代理,來實作對jdk中線程池中runnable/callable的代理實作,具體可以參考以下連結:

Java SE 6 新特性Instrumentation

TTL基于Instrumentation的實作示例代碼如下:

public static void premain(String agentArgs, Instrumentation inst) {
        //這裡就是TTL的代理實作,預設加入了
        //TtlExecutorTransformlet和TtlForkJoinTransformlet
        //而以上兩個類的對應代理為下面代碼所示
        TtlAgent.premain(agentArgs, inst); // add TTL Transformer

        // add your Transformer
        ...
    }
}
    //#######下面代碼是TtlExecutorTransformlet########
    //#######可以看出支援了ThreadPoolExecutor和ScheduledThreadPoolExecutor兩個線程池
	private static Set<String> EXECUTOR_CLASS_NAMES = new HashSet<String>();
    private static final Map<String, String> PARAM_TYPE_NAME_TO_DECORATE_METHOD_CLASS = new HashMap<String, String>();

    static {
        EXECUTOR_CLASS_NAMES.add("java.util.concurrent.ThreadPoolExecutor");
        EXECUTOR_CLASS_NAMES.add("java.util.concurrent.ScheduledThreadPoolExecutor");

        PARAM_TYPE_NAME_TO_DECORATE_METHOD_CLASS.put("java.lang.Runnable", "com.alibaba.ttl.TtlRunnable");
        PARAM_TYPE_NAME_TO_DECORATE_METHOD_CLASS.put("java.util.concurrent.Callable", "com.alibaba.ttl.TtlCallable");
    }
           

5. 舉例:hystrix線上程隔離模式下的上下文變量擷取

說明

如果不使用TTL提供的agent,在使用hystrix做容錯處理時,就會出現上面所說的線程變量錯亂讀取的問題,并且hystrix是有自己管理的線程池的。

這時候顯然是要使用TTL的,但是該如何将TTL中的Runnable或Callable內建到

hystrix中的線程池中呢?

HystrixConcurrencyStrategy這個類hystrix擷取線程池的關鍵類,并且可以自定義實作,

HystrixConcurrencyStrategy裡面包含的方法有getThreadPool/getThreadFactory/getBlockingQueue,這三個方法的提供為其擷取

線程池提供了很大的自由度。

使用方法HystrixPlugins.getInstance().registerConcurrencyStrategy(your concurrency impl);

在使用SleuthHystrixConcurrencyStrategy接入的例子

@Component
public class MySleuthHystrixConcurrencyStrategy extends SleuthHystrixConcurrencyStrategy {

    public MySleuthHystrixConcurrencyStrategy(Tracer tracer, TraceKeys traceKeys) {
        super(tracer, traceKeys);
    }

    //包裝sleuth給出的callable
    public <T> Callable<T> wrapCallable(Callable<T> callable) {
        Callable<T> call = super.wrapCallable(callable);
        //進行ttl包裝
        return TtlCallable.get(call);
    }

}
           

寫在後面

因為這邊文章主要介紹的就是關于線程變量ThreadLocal的原理和應用,是以很多地方都沒擴散開來,而且這次也是比較倉促,以後有空閑了也可能進行補充;後面有機會的話,可能還會有:

  • java線程那點事兒
  • java線程池那點兒事兒
  • java類加載器那點兒事兒

littlehow 寫于2018-10-25 ~ 2018-10-26

附錄

線程狀态說明

傳回

狀态碼 狀态 說明
(初始)NEW 新建立了一個線程對象,但還沒有調用start()方法
1,4 (運作)RUNNABLE Java線程中将就緒(ready)和運作中(running)兩種狀态籠統的稱為“運作”
1024 阻塞(BLOCKED) 表示線程阻塞于鎖
16 等待(WAITING) 進入該狀态的線程需要等待其他線程做出一些特定動作(通知或中斷)
32 逾時等待(TIMED_WAITING) 該狀态不同于WAITING,它可以在指定的時間後自行傳回
2 終止(TERMINATED) 表示該線程已經執行完畢