1. 程式人生 > 程式設計 >java執行緒變數那點事兒

java執行緒變數那點事兒

寫在前面

該文章主要介紹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,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,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,birthday=1990-03-07)
littlehow-2新增使用者資訊:User(userId=6,birthday=1989-11-10)
littlehow-3新增使用者資訊:User(userId=3,birthday=1995-07-26)
littlehow-1新增使用者資訊:User(userId=2,birthday=1990-03-07)
littlehow-3新增使用者資訊:User(userId=3,birthday=1995-07-26)
littlehow-2新增使用者資訊:User(userId=6,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,birthday=1990-03-07)
littlehow-1新增使用者資訊:User(userId=7,birthday=1990-03-07)
littlehow-1新增使用者資訊:User(userId=10,birthday=1988-09-11)
littlehow-3新增使用者資訊:User(userId=1,birthday=1989-11-10)
littlehow-1新增使用者資訊:User(userId=5,birthday=1988-09-11)
littlehow-3新增使用者資訊:User(userId=6,birthday=1989-11-10)
littlehow-1新增使用者資訊:User(userId=8,birthday=1995-07-26)
littlehow-3新增使用者資訊:User(userId=3,birthday=1995-07-26)
littlehow-1新增使用者資訊:User(userId=9,birthday=2000-10-01)
littlehow-2新增使用者資訊:User(userId=4,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);

寫在後面

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

  • java執行緒那點事兒
  • java執行緒池那點兒事兒
  • java類載入器那點兒事兒

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

附錄

執行緒狀態說明

返回

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