1. 程式人生 > >Java面試題必備知識之ThreadLocal

Java面試題必備知識之ThreadLocal

老套路,先列舉下關於ThreadLocal常見的疑問,希望可以通過這篇學習筆記來解決這幾個問題:

  1. ThreadLocal是用來解決什麼問題的?
  2. 如何使用ThreadLocal?
  3. ThreadLocal的實現原理是什麼?
  4. 可否舉幾個實際專案中使用ThreadLocal的案例?

基礎知識

ThreadLocal是執行緒區域性變數,和普通變數的不同在於:每個執行緒持有這個變數的一個副本,可以獨立修改(set方法)和訪問(get方法)這個變數,並且執行緒之間不會發生衝突。

類中定義的ThreadLocal例項一般會被private static修飾,這樣可以讓ThreadLocal例項的狀態和Thread繫結在一起,業務上,一般用ThreadLocal包裝一些業務ID(user ID或事務ID)——不同的執行緒使用的ID是不相同的。

如何使用

case1

從某個角度來看,ThreadLocal為Java併發程式設計提供了額外的思路——避免併發,如果某個物件本身是非執行緒安全的,但是你想實現多執行緒同步訪問的效果,例如SimpleDateFormat,你可以使用ThreadLocal變數。

public class Foo
{
    // SimpleDateFormat is not thread-safe, so give one to each thread
    private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){
        @Override
        protected SimpleDateFormat initialValue()
        {
            return new SimpleDateFormat("yyyyMMdd HHmm");
        }
    };

    public String formatIt(Date date)
    {
        return formatter.get().format(date);
    }
}

注意,這裡針對每個執行緒只需要初始化一次SimpleDateFormat物件,其實跟在自定義執行緒中定義一個SimpleDateFormat成員變數,並在執行緒初始化的時候new這個物件,效果是一樣的,只是這樣看起來程式碼更規整。

case2

之前在yunos做酷盤專案的資料遷移時,我們需要按照使用者維度去加鎖,每個執行緒在處理遷移之前,都需要先獲取當前使用者的鎖,每個鎖的key是帶著使用者資訊的,因此也可以使用ThreadLocal變數實現:

case3

下面這個例子,我們定義了一個MyRunnable物件,這個MyRunnable物件會被執行緒1和執行緒2使用,但是通過內部的ThreadLocal變數,每個執行緒訪問到的整數都是自己單獨的一份。

package org.java.learn.concurrent.threadlocal;

/**
 * @author duqi
 * @createTime 2018-12-29 23:25
 **/
public class ThreadLocalExample {
    public static class MyRunnable implements Runnable {

        private ThreadLocal<Integer> threadLocal =
                new ThreadLocal<Integer>();

        @Override
        public void run() {
            threadLocal.set((int) (Math.random() * 100D));

            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
            }

            System.out.println(threadLocal.get());
        }
    }


    public static void main(String[] args) throws InterruptedException {
        MyRunnable sharedRunnableInstance = new MyRunnable();

        Thread thread1 = new Thread(sharedRunnableInstance);
        Thread thread2 = new Thread(sharedRunnableInstance);

        thread1.start();
        thread2.start();

        thread1.join(); //wait for thread 1 to terminate
        thread2.join(); //wait for thread 2 to terminate
    }
}

ThreadLocal關鍵知識點

原始碼分析

ThreadLocal是如何被執行緒使用的?原理如下圖所示:Thread引用和ThreadLocal引用都在棧上,Thread引用會引用一個ThreadLocalMap物件,這個map中的key是ThreadLocal物件(使用WeakReference包裝),value是業務上變數的值。

首先看java.lang.Thread中的程式碼:

public
class Thread implements Runnable {
    //......其他原始碼
    /* ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    //......其他原始碼

Thread中的threadLocals變數指向的是一個map,這個map就是ThreadLocal.ThreadLocalMap,裡面存放的是跟當前執行緒繫結的ThreadLocal變數;inheritableThreadLocals的作用相同,裡面也是存放的ThreadLocal變數,但是存放的是從當前執行緒的父執行緒繼承過來的ThreadLocal變數。

在看java.lang.ThreadLocal類,主要的成員和介面如下:

  1. withInitial方法,Java 8以後用於初始化ThreadLocal的一種方法,在外部呼叫get()方法的時候,會通過Supplier確定變數的初始值;

    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }
  2. get方法,獲取當前執行緒的變數副本,如果當前執行緒還沒有建立該變數的副本,則需要通過呼叫initialValue方法來設定初始值;get方法的原始碼如下,首先通過當前執行緒獲取當前執行緒對應的map,如果map不為空,則從map中取出對應的Entry,然後取出對應的值;如果map為空,則呼叫setInitialValue設定初始值;如果map不為空,當前ThreadLocal例項對應的Entry為空,則也需要設定初始值。

    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();
    }
  3. set方法,跟get方法一樣,先獲取當前執行緒對應的map,如果map為空,則呼叫createMap建立map,否則將變數的值放入map——key為當前這個ThreadLocal物件,value為變數的值。

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
  4. remove方法,刪除當前執行緒繫結的這個副本

         public void remove() {
             ThreadLocalMap m = getMap(Thread.currentThread());
             if (m != null)
                 m.remove(this);
         }
  5. 數字0x61c88647,這個值是HASH_INCREMENT的值,普通的hashmap是使用連結串列來處理衝突的,但是ThreadLocalMap是使用線性探測法來處理衝突的,HASH_INCREMENT就是每次增加的步長,根據參考資料1所說,選擇這個數字是為了讓衝突概率最小。

    /**
     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     */
    private static final int HASH_INCREMENT = 0x61c88647;

父子程序資料共享

InheritableThreadLocal主要用於子執行緒建立時,需要自動繼承父執行緒的ThreadLocal變數,實現子執行緒訪問父執行緒的threadlocal變數。InheritableThreadLocal繼承了ThreadLocal,並重寫了childValue、getMap、createMap三個方法。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    /**
     * 建立執行緒的時候,如果需要繼承且父執行緒中Thread-Local變數,則需要將父執行緒中的ThreadLocal變數一次拷貝過來。
     */
    protected T childValue(T parentValue) {
        return parentValue;
    }

    /**
    * 由於重寫了getMap,所以在操作InheritableThreadLocal變數的時候,將只操作Thread類中的inheritableThreadLocals變數,與threadLocals變數沒有關係
    **/
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    /**
     * 跟getMap類似,set或getInheritableThreadLocal變數的時候,將只操作Thread類中的inheritableThreadLocals變數
     */
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

關於childValue多說兩句,拷貝是如何發生的?
首先看Thread.init方法,

    private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) {
        //其他原始碼
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

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

然後看ThreadLocal.createInheritedMap方法,最終會呼叫到newThreadLocalMap方法,這裡InheritableThreadLocal對childValue做了重寫,可以看出,這裡確實是將父執行緒關聯的ThreadLocalMap中的內容依次拷貝到子執行緒的ThreadLocalMap中了。

       private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

ThreadLocal物件何時被回收?

ThreadLocalMap中的key是ThreadLocal物件,然後ThreadLocal物件時被WeakReference包裝的,這樣當沒有強引用指向該ThreadLocal物件之後,或者說Map中的ThreadLocal物件被判定為弱引用可達時,就會在垃圾收集中被回收掉。看下Entry的定義:

 static class Entry extends WeakReference<ThreadLocal<?>> {
     /** The value associated with this ThreadLocal. */
     Object value;

     Entry(ThreadLocal<?> k, Object v) {
         super(k);
         value = v;
     }
}

ThreadLocal和執行緒池一起使用?

ThreadLocal物件的生命週期跟執行緒的生命週期一樣長,那麼如果將ThreadLocal物件和執行緒池一起使用,就可能會遇到這種情況:一個執行緒的ThreadLocal物件會和其他執行緒的ThreadLocal物件串掉,一般不建議將兩者一起使用。

案例學習

Dubbo中對ThreadLocal的使用

我從Dubbo中找到了ThreadLocal的例子,它主要是用在請求快取的場景,具體程式碼如下:

@Activate(group = {Constants.CONSUMER, Constants.PROVIDER}, value = Constants.CACHE_KEY)
public class CacheFilter implements Filter {

    private CacheFactory cacheFactory;

    public void setCacheFactory(CacheFactory cacheFactory) {
        this.cacheFactory = cacheFactory;
    }

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        if (cacheFactory != null && ConfigUtils.isNotEmpty(invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.CACHE_KEY))) {
            Cache cache = cacheFactory.getCache(invoker.getUrl(), invocation);
            if (cache != null) {
                String key = StringUtils.toArgumentString(invocation.getArguments());
                Object value = cache.get(key);
                if (value != null) {
                    if (value instanceof ValueWrapper) {
                        return new RpcResult(((ValueWrapper)value).get());
                    } else {
                        return new RpcResult(value);
                    }
                }
                Result result = invoker.invoke(invocation);
                if (!result.hasException()) {
                    cache.put(key, new ValueWrapper(result.getValue()));
                }
                return result;
            }
        }
        return invoker.invoke(invocation);
    }

可以看出,在RPC呼叫(invoke)的鏈路上,會先使用請求引數判斷當前執行緒是否剛剛發起過同樣引數的呼叫——這個呼叫會使用ThreadLocalCache儲存起來。具體的看,ThreadLocalCache的實現如下:

package org.apache.dubbo.cache.support.threadlocal;

import org.apache.dubbo.cache.Cache;
import org.apache.dubbo.common.URL;

import java.util.HashMap;
import java.util.Map;

/**
 * ThreadLocalCache
 */
public class ThreadLocalCache implements Cache {

    //ThreadLocal裡存放的是引數到結果的對映
    private final ThreadLocal<Map<Object, Object>> store;

    public ThreadLocalCache(URL url) {
        this.store = new ThreadLocal<Map<Object, Object>>() {
            @Override
            protected Map<Object, Object> initialValue() {
                return new HashMap<Object, Object>();
            }
        };
    }

    @Override
    public void put(Object key, Object value) {
        store.get().put(key, value);
    }

    @Override
    public Object get(Object key) {
        return store.get().get(key);
    }

}

RocketMQ

在RocketMQ中,我也找到了ThreadLocal的身影,它是用在訊息傳送的場景,MQClientAPIImpl是RMQ中負責將訊息傳送到服務端的實現,其中有一個步驟需要選擇一個具體的佇列,選擇具體的佇列的時候,不同的執行緒有自己負責的index值,這裡使用了ThreadLocal的機制,可以看下ThreadLocalIndex的實現:

package org.apache.rocketmq.client.common;

import java.util.Random;

public class ThreadLocalIndex {
    private final ThreadLocal<Integer> threadLocalIndex = new ThreadLocal<Integer>();
    private final Random random = new Random();

    public int getAndIncrement() {
        Integer index = this.threadLocalIndex.get();
        if (null == index) {
            index = Math.abs(random.nextInt());
            if (index < 0)
                index = 0;
            this.threadLocalIndex.set(index);
        }

        index = Math.abs(index + 1);
        if (index < 0)
            index = 0;

        this.threadLocalIndex.set(index);
        return index;
    }

    @Override
    public String toString() {
        return "ThreadLocalIndex{" +
            "threadLocalIndex=" + threadLocalIndex.get() +
            '}';
    }
}

總結

這篇文章主要是解決了關於ThreadLocal的幾個問題:(1)具體的概念是啥?(2)在Java開發中的什麼場景下使用?(3)ThreadLocal的實現原理是怎樣的?(4)開源專案中有哪些案例可以參考?不知道你是否對這幾個問題有了一定的瞭解呢?如果還有疑問,歡迎交流。

參考資料

  1. Why 0x61c88647?
  2. Java ThreadLocal
  3. When and how should I use a ThreadLocal variable?
  4. 技術小黑屋:理解Java中的ThreadLocal
  5. 深入分析ThreadLocal的記憶體洩漏問題
  6. 《Java併發程式設計實戰》
  7. InheritableThreadLocal詳解
  8. ThreadLocal詳解
  9. ThreadLocal的使用場景
  10. 資料結構:雜湊表

本號專注於後端技術、JVM問題排查和優化、Java面試題、個人成長和自我管理等主題,為讀者提供一線開發者的工作和成長經驗,期待你能在這裡有所收穫。

相關推薦

Java試題必備知識ThreadLocal

老套路,先列舉下關於ThreadLocal常見的疑問,希望可以通過這篇學習筆記來解決這幾個問題: ThreadLocal是用來解決什麼問題的? 如何使用ThreadLocal? ThreadLocal的實現原理是什麼? 可否舉幾個實際專案中使用ThreadLocal的案例? 基礎知識 ThreadLo

Java試題-基礎知識

本文來源於清英的一篇文章:你應該知道的JAVA面試題,最近自己也在面試一些候選人,發現這篇文章中的有些點我也拿不準,因此按照自己的理解整理一份參考回答。 基礎題目 1. Java執行緒的狀態 Java執行緒在某個時刻只能處於以下六個狀態中的一個。 – New(新建立),一個執行緒剛剛被創建出

超詳細的Java試題總結(二)Java基礎知識

多執行緒和Java虛擬機器 建立執行緒有幾種不同的方式?你喜歡哪一種?為什麼? 繼承Thread類 實現Runnable介面 應用程式可以使用Executor框架來建立執行緒池 實現Callable介面。 我更喜歡實現Runnable介面這種方法,當然這也是現

超詳細的Java試題總結(一)Java基礎知識

福利:看本文之前,推薦給大家一個阿里雲雙11活動,真的非常非常非常推薦,對於新人福利,阿里雲這次真的是下血本了,建議阿里雲新人一定一定一定不要錯過。如果覺得這單純是廣告的話(阿里雲肯找我做廣告就好了,嘿嘿),你可以直接跳過看正文。 阿里雲雙11最新活動(僅限阿

java試題什麼是執行緒ThreadLocal?作用是什麼?

定義:執行緒區域性變數是侷限於執行緒內的變數,屬於執行緒自身所有,不在多個執行緒間共享。java提供ThreadLocal類來支援執行緒區域性變數,是一個實現執行緒安全的方式。 任何執行緒區域性變數一旦在工作完成後沒有釋放,java應用就存在記憶體洩露的風險   作用:ThreadLocal是

java試題int和Integer的區別

4條 試題 als 自動 數據類型 類型 fort spec 新建 int和Integer的區別 1、Integer是int的包裝類,int則是java的一種基本數據類型 2、Integer變量必須實例化後才能使用,而int變量不需要 3、Integer實際是對象的引用,

Java試題Java基礎

formate spa 多重繼承 輸出 不能 類名 gre collect extend 1、作用域public,private,protected,以及不寫時的區別 答:區別如下: 作用域 當前類 同一package 子孫類

java試題----HashMap常見面試題總結

使用 這一 hashtable 此刻 獲取 重要性 microsoft ria 取值 “你用過HashMap嗎?” “什麽是HashMap?你為什麽用到它?” 幾乎每個人都會回答“是的”,然後回答HashMap的一些特性,譬如HashMap可以接受null鍵值和值,而Has

java試題----mysql表優化方案

mysql 引擎 它的 易維 range 概述 .com 設置 常用 本文轉載自segmentfault,原文鏈接:https://segmentfault.com/a/1190000006158186。 當MySQL單表記錄數過大時,增刪改查性能都會急劇下降,可以參考以

java試題----jdbc中使用的設計模式(橋接模式)

connect @override 操作 tro orb his order ng- return 1.JDBC(JavaDatabase Connectivity) JDBC是以統一方式訪問數據庫的API. 它提供了獨立於平臺的數據庫訪問,也就是說,有了JDB

2018年最新JAVA試題總結資料庫(3)

轉自於:https://zhuanlan.zhihu.com/p/39804394   1、MySQL的delete與truncate區別? 回答:delete語句執行刪除的過程是每次從表中刪除一行,並且同時將該行的刪除操作作為事務記錄在日誌中儲存以便進行回滾操作,不清空AUTO_INCREM

2018年最新JAVA試題總結基礎(1)

轉自於:https://zhuanlan.zhihu.com/p/39322967   1、JAVA中能建立volatile陣列嗎?volatile能使得一個非原子操作變成原子操作嗎? 回答: 能,Java 中可以建立 volatile 型別陣列,不過只是一個指向陣列的引用,而不是整個陣列。J

2018年最新JAVA試題總結框架(4)

轉自於:https://zhuanlan.zhihu.com/p/40098726   1、談談對spring框架的瞭解 ,spring有什麼作用(IOC,AOP),spring的核心是什麼?回答:Spring是一個開源框架,它是為了解決企業應用開發的複雜性而建立的。框架的主要優勢之一就是其分層

那些java試題資料庫常問的!你會了多少!

資料庫裡面的索引和約束是什麼 用SQL語句實現oracle分頁查詢。   小編是一個有著5年工作經驗的java程式設計師,對於java,自己有做資料的整合,一個完整學習java的路線,學習資料和工具,相信這裡有很多學習java的小夥伴,我創立了一個20

java試題Thread的run()和start()方法有什麼區別

run()方法:   是在主執行緒中執行方法,和呼叫普通方法一樣;(按順序執行,同步執行) start()方法:   是建立了新的執行緒,在新的執行緒中執行;(非同步執行)   public class App { public static void main( Stri

java試題sleep()和wait()方法的區別

sleep方法:   屬於Thread類中的方法;會導致程式暫停執行指定的時間,讓出cpu該其他執行緒,但是他的監控狀態依然保持著,當指定時間到了之後,又會自動恢復執行狀態;在呼叫sleep方法的過程中,執行緒不會釋放物件鎖。 wait方法:   屬於Object類中的方法;在呼叫wait方法的時候,執

java試題什麼是死鎖、活鎖、餓死和競態條件?

    死鎖:是指兩個或兩個以上的程序(或執行緒)在執行過程中,因爭奪資源而造成的一種相互等待的現象,若無外力作用,他們將無法推進下去; 活鎖:是指兩個執行緒優先順序相同,都禮讓不走,就這樣一直僵持下去; 餓死:在單執行緒情況下,A、B兩個執行緒,A先執行;A在執行過程中

java試題簡單介紹一下集合框架

klist linklist lin key 很多 array 們的 簡單 數據存儲 集合框架分為三塊:List列表、Set集合、Map映射 List列表在數據結構上可以被看做線性表,常用的有ArrayList和LinkList(不常用的有Vector(類似於Arra

java試題synchronized和lock有什麽區別

作用範圍 out inter mutex 虛擬 add moni 私有 p s synchronized和lock的區別: 類別 synchronized lock 存在層次 java的關鍵字,在jvm層面上 是一個類 鎖的釋放

超詳細的Java試題總結(三)Java集合篇常見問題

List,Set,Map三者的區別及總結 List:對付順序的好幫手 List介面儲存一組不唯一(可以有多個元素引用相同的物件),有序的物件 Set:注重獨一無二的性質 不允許重複的集合。不會有多個元素引用相同的物件。 Map:用Key來搜尋的專家 使用鍵值對儲存。M