美團面試問我ThreadLocal原理,我這麼回答經過了面試!

上週我僥倖經過美團一面,崗位是java後端開發工程師。java

美團面試官給我進行了二面。面試

面試過程當中他問了ThreadLocal原理(上次問線程池,此次問ThreadLocal,美團爸爸這麼喜歡線程安全機制麼),今天詳細講一講ThreadLocal原理。spring

ThreadLocal

ThreadLocal是線程的內部存儲類,能夠在指定線程內存儲數據。只有指定線程能夠獲得存儲數據。後端

/**
 * 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.,
 * a user ID or Transaction ID).
 */

每一個線程都有一個ThreadLocalMap的實例對象,而且經過ThreadLocal管理ThreadLocalMap。設計模式

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

每一個新線程都會實例化爲一個ThreadLocalMap而且賦值給成員變量ThreadLocals,使用時若已經存在threadLocals則直接使用已經存在的對象。數組

應用場景安全

當某些數據是以線程爲做用域而且不一樣線程有不一樣數據副本時,考慮ThreadLocal。網絡

無狀態,副本變量獨立後不影響業務邏輯的高併發場景。數據結構

若是若是業務邏輯強依賴於副本變量,則不適合用ThreadLocal解決。併發

get()與set()

set()是調用ThreadLocalMap的set()實現的

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
//getMap方法
ThreadLocalMap getMap(Thread t) {
      //thred中維護了一個ThreadLocalMap
      return t.threadLocals;
 }
 
//createMap
void createMap(Thread t, T firstValue) {
      //實例化一個新的ThreadLocalMap,並賦值給線程的成員變量threadLocals
      t.threadLocals = new ThreadLocalMap(this, firstValue);
}

ThreadLocalMap

ThreadLocalMap爲每一個Thread都維護了一個數組table,ThreadLocal肯定了一個數組下標,而這個下標是value存儲的對應位置。

file

ThreadLocalMaps是延遲構造的,所以只有在至少要放置一個條目時才建立。

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

ThreadLocalMap初始化時建立了默認長度是16的Entry數組。經過hashCode與length位運算肯定索引值i。

每一個Thread都有一個ThreadLocalMap類型。至關於每一個線程Thread都有一個Entry型的數組table。而一切讀取過程都是經過操做這個數組table進行的。

set() 方法

private void set(ThreadLocal<?> key, Object value) {
   // We don't use a fast path as with get() because it is at
   // least as common to use set() to create new entries as
   // it is to replace existing ones, in which case, a fast
   // path would fail more often than not.
​
   Entry[] tab = table;
   int len = tab.length;
   //經過&運算計算索引
   int i = key.threadLocalHashCode & (len-1);
​
   for (Entry e = tab[i];
        e != null;
        e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        //若是存在key則覆蓋
        if (k == key) {
            e.value = value;
            return;
        }
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
   }
   //新建結點插入
   tab[i] = new Entry(key, value);
   int sz = ++size;
   if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

將threadLocalHashCode與長度進行位運算獲得索引。

threadLocalHashCode的代碼以下:

private final int threadLocalHashCode = nextHashCode();
​
    /**
     * The next hash code to be given out. Updated atomically. Starts at
     * zero.
     */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();
​
    /**
     * 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;
​
    /**
     * Returns the next hash code.
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

因爲是static變量,threadLocalHashCode在每次加載threadLocal類時會從新初始化,同時會自增一次,增長HASH_INCREMENT(斐波那契散列乘數,經過該數散列出來的結果會比較均勻)。

static變量也稱做靜態變量,靜態變量和非靜態變量的區別是:靜態變量被全部的對象所共享,在內存中只有一個副本,它當且僅當在類初次加載時會被初始化。

而非靜態變量是對象所擁有的,在建立對象的時候被初始化,存在多個副本,各個對象擁有的副本互不影響。static成員變量的初始化順序按照定義的順序進行初始化。

對於一個ThreadLocal來說,他的索引值i是肯定的。對於不一樣線程,同一個threadlocal對應的是不一樣table的同一下標,便是table[i],不一樣線程之間的table是相互獨立的。

get() 方法

計算索引,直接取出

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();
    }

remove() 方法

/**
  * Remove the entry for key.
  */
   private void remove(ThreadLocal<?> key) {
       Entry[] tab = table;
       int len = tab.length;
       int i = key.threadLocalHashCode & (len-1);
       for (Entry e = tab[i];
            e != null;
            e = tab[i = nextIndex(i, len)]) {
           if (e.get() == key) {
               e.clear();
               expungeStaleEntry(i);
               return;
           }
       }
   }
​

線程隔離特性

線程隔離特性,只有在線程內才能獲取到對應的值,線程外不能訪問。

(1)Synchronized是經過線程等待,犧牲時間來解決訪問衝突

(1)ThreadLocal是經過每一個線程單獨一份存儲空間,犧牲空間來解決衝突

內存泄露問題

存在內存泄露問題,每次使用完ThreadLocal,都調用它的remove()方法,清除數據。

Demo程序

import java.util.concurrent.atomic.AtomicInteger;
​
/**
 * <h3>Exper1</h3>
 * <p>ThreadLocalId</p>
 *
 * @author : cxc
 * @date : 2020-04-01 23:48
 **/
  public class ThreadLocalId {
      // Atomic integer containing the next thread ID to be assigned
          private static final AtomicInteger nextId = new AtomicInteger(0);
​
          // Thread local variable containing each thread's ID
          private static final ThreadLocal <Integer> threadId =
              new ThreadLocal<Integer>()
          {
              @Override
              protected Integer initialValue() {
                  return nextId.getAndIncrement();
              }
          };
​
          // Returns the current thread's unique ID, assigning it if necessary
          public static int get() {
            return threadId.get();
          }
          public static void remove() {
            threadId.remove();
          }
  }
​
/**
 * <h3>Exper1</h3>
 * <p></p>
 *
 * @author : cxc
 * @date : 2020-04-02 00:07
 **/
public class ThreadLocalMain {
  private static void incrementSameThreadId(){
    try{
      for(int i=0;i<5;i++){
        System.out.println(Thread.currentThread()
        +"_"+i+",threadId:"+
            ThreadLocalId.get());
      }
    }finally {
      ThreadLocalId.remove();
    }
  }
​
  public static void main(String[] args) {
    incrementSameThreadId();
    new Thread(new Runnable() {
      @Override
      public void run() {
        incrementSameThreadId();
      }
    }).start();
    new Thread(new Runnable() {
      @Override
      public void run() {
        incrementSameThreadId();
      }
    }).start();
  }
}

總結

ThreadLocal的原理在面試中幾乎被問爛了。

Thread的私有數據是存儲在ThreadLocalMap,經過ThreadLoacl進行管理。

要了解ThreadLocal的原理,最好多閱讀幾遍源碼,尤爲是ThreadLocalMap的源碼部分。你們面試前要把知識點記牢。

文源網絡,僅供學習之用,若有侵權,聯繫刪除。

我將面試題和答案都整理成了PDF文檔,還有一套學習資料,涵蓋Java虛擬機、spring框架、Java線程、數據結構、設計模式等等,但不只限於此。

關注公衆號【java圈子】獲取資料,還有優質文章每日送達。

file

相關文章
相關標籤/搜索