首先看一个使用的案例

  1. public class Temp {
  2. public static void main(String[] args) {
  3. // 新建一个ThreadLocal
  4. ThreadLocal<String> local = new ThreadLocal<>();
  5. Random random = new Random();
  6. //使用Stream新建5个线程
  7. IntStream.range(0,5).forEach(a-> new Thread(() -> {
  8. //为每个线程设置相应的local值
  9. local.set(a +" "+ random.nextInt(10));
  10. System.out.println("线程 and local值 = " + local.get() +", " +local.hashCode());
  11. try {
  12. TimeUnit.SECONDS.sleep(1);
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. }).start());
  17. }
  18. }

结果:

线程 and local值 = 0 6, 936213316 线程 and local值 = 1 6, 936213316 线程 and local值 = 3 2, 936213316

线程 and local值 = 2 8, 936213316

线程 and local值 = 4 9, 936213316

ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

图解

源码部分

set方法

  1. public void set(T value) {
  2. //获取当前调用该方法的线程对象
  3. Thread t = Thread.currentThread();
  4. //获取当前线程的ThreadLocalMap的实例对象map
  5. ThreadLocalMap map = getMap(t);
  6. if (map != null)
  7. 如果map已存在,则将当前ThreadLocal作为key,要存储的对象作为value存储到map
  8. map.set(this, value);
  9. else
  10. 如果mapnull,则创建map对象
  11. createMap(t, value);
  12. }
  13. ThreadLocalMap getMap(Thread t) {
  14. return t.threadLocals; //ThreadLocal.ThreadLocalMap threadLocals = null;
  15. }
  16. void createMap(Thread t, T firstValue) {
  17. t.threadLocals = new ThreadLocalMap(this, firstValue);
  18. }
  19. //ThreadLocalMap的构造方法
  20. ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
  21. //初始化table
  22. table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
  23. //计算索引
  24. int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  25. //设置值
  26. table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
  27. size = 1;
  28. //设置阈值
  29. setThreshold(INITIAL_CAPACITY);
  30. }


ThreadLocalMap.set方法

ThreadLocalMap使用线性探测法来解决哈希冲突,线性探测法的地址增量di = 1, 2, … , m-1,其中,i为探测次数。该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。

线性探测相关的代码

  1. /**
  2. * 获取环形数组的下一个索引
  3. */
  4. private static int nextIndex(int i, int len) {
  5. return ((i + 1 < len) ? i + 1 : 0);
  6. }
  7. /**
  8. * 获取环形数组的上一个索引
  9. */
  10. private static int prevIndex(int i, int len) {
  11. return ((i - 1 >= 0) ? i - 1 : len - 1);
  12. }

set()及其相关代码(主要看1-41行)
一个ThreadLocal相当于一个隔离变量

  • 向ThreadLocalMap中插入元素(ThreadLocal t , Object v)时会计算元素的hash值
  • 然后利用hash值 &(len-1)找到bucket
  • 如果bucket为空则插入,不为空则判断当前bucket的key是否与待插入的key相同;
  • 如果不能插入,则查看bucket+1的位置
  • 如果找到hash表的最后一个bucket也不能插入,则回到0号bucket进行判断。 ```java private void set(ThreadLocal<?> key, Object value) {

    1. ThreadLocal.ThreadLocalMap.Entry[] tab = table;
    2. int len = tab.length;
    3. //计算索引,上面已经有说过。
    4. int i = key.threadLocalHashCode & (len-1);
    5. /**
    6. * 根据获取到的索引进行循环,如果当前索引上的table[i]不为空,在没有return的情况下,
    7. * 就使用nextIndex()获取下一个(上面提到到线性探测法)。
    8. */
    9. for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
    10. e != null;
    11. e = tab[i = nextIndex(i, len)]) {
    12. ThreadLocal<?> k = e.get();
    13. //table[i]上key不为空,并且和当前key相同,更新value
    14. if (k == key) {
    15. e.value = value;
    16. return;
    17. }
    18. /**
    19. * table[i]上的key为空,说明被回收了(上面的弱引用中提到过)。
    20. * 这个时候说明改table[i]可以重新使用,用新的key-value将其替换,并删除其他无效的entry
    21. */
    22. if (k == null) {
    23. replaceStaleEntry(key, value, i);
    24. return;
    25. }
    26. }
    27. //找到为空的插入位置,插入值,在为空的位置插入需要对size进行加1操作
    28. tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
    29. int sz = ++size;
    30. /**
    31. * cleanSomeSlots用于清除那些e.get()==null,也就是table[index] != null && table[index].get()==null
    32. * 之前提到过,这种数据key关联的对象已经被回收,所以这个Entry(table[index])可以被置null。
    33. * 如果没有清除任何entry,并且当前使用量达到了负载因子所定义(长度的2/3),那么进行rehash()
    34. */
    35. if (!cleanSomeSlots(i, sz) && sz >= threshold)
    36. rehash();

    }

/**
 * 替换无效entry
 */
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
    ThreadLocal.ThreadLocalMap.Entry[] tab = table;
    int len = tab.length;
    ThreadLocal.ThreadLocalMap.Entry e;

    /**
     * 根据传入的无效entry的位置(staleSlot),向前扫描
     * 一段连续的entry(这里的连续是指一段相邻的entry并且table[i] != null),
     * 直到找到一个无效entry,或者扫描完也没找到
     */
    int slotToExpunge = staleSlot;//之后用于清理的起点
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

    /**
     * 向后扫描一段连续的entry
     */
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        /**
         * 如果找到了key,将其与传入的无效entry替换,也就是与table[staleSlot]进行替换
         */
        if (k == key) {
            e.value = value;

            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;

            //如果向前查找没有找到无效entry,则更新slotToExpunge为当前值i
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        /**
         * 如果向前查找没有找到无效entry,并且当前向后扫描的entry无效,则更新slotToExpunge为当前值i
         */
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    /**
     * 如果没有找到key,也就是说key之前不存在table中
     * 就直接最开始的无效entry——tab[staleSlot]上直接新增即可
     */
    tab[staleSlot].value = null;
    tab[staleSlot] = new ThreadLocal.ThreadLocalMap.Entry(key, value);

    /**
     * slotToExpunge != staleSlot,说明存在其他的无效entry需要进行清理。
     */
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

/**
 * 连续段清除
 * 根据传入的staleSlot,清理对应的无效entry——table[staleSlot],
 * 并且根据当前传入的staleSlot,向后扫描一段连续的entry(这里的连续是指一段相邻的entry并且table[i] != null),
 * 对可能存在hash冲突的entry进行rehash,并且清理遇到的无效entry.
 *
 * @param staleSlot key为null,需要无效entry所在的table中的索引
 * @return 返回下一个为空的solt的索引。
 */
private int expungeStaleEntry(int staleSlot) {
    ThreadLocal.ThreadLocalMap.Entry[] tab = table;
    int len = tab.length;

    // 清理无效entry,置空
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    //size减1,置空后table的被使用量减1
    size--;

    ThreadLocal.ThreadLocalMap.Entry e;
    int i;
    /**
     * 从staleSlot开始向后扫描一段连续的entry
     */
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        //如果遇到key为null,表示无效entry,进行清理.
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            //如果key不为null,计算索引
            int h = k.threadLocalHashCode & (len - 1);
            /**
             * 计算出来的索引——h,与其现在所在位置的索引——i不一致,置空当前的table[i]
             * 从h开始向后线性探测到第一个空的slot,把当前的entry挪过去。
             */
            if (h != i) {
                tab[i] = null;
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    //下一个为空的solt的索引。
    return i;
}

/**
 * 启发式的扫描清除,扫描次数由传入的参数n决定
 *
 * @param i 从i向后开始扫描(不包括i,因为索引为i的Slot肯定为null)
 *
 * @param n 控制扫描次数,正常情况下为 log2(n) ,
 * 如果找到了无效entry,会将n重置为table的长度len,进行段清除。
 *
 * map.set()点用的时候传入的是元素个数,replaceStaleEntry()调用的时候传入的是table的长度len
 *
 * @return true if any stale entries have been removed.
 */
private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    ThreadLocal.ThreadLocalMap.Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len);
        ThreadLocal.ThreadLocalMap.Entry e = tab[i];
        if (e != null && e.get() == null) {
            //重置n为len
            n = len;
            removed = true;
            //依然调用expungeStaleEntry来进行无效entry的清除
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);//无符号的右移动,可以用于控制扫描次数在log2(n)
    return removed;
}


private void rehash() {
    //全清理
    expungeStaleEntries();

    /**
     * threshold = 2/3 * len
     * 所以threshold - threshold / 4 = 1en/2
     * 这里主要是因为上面做了一次全清理所以size减小,需要进行判断。
     * 判断的时候把阈值调低了。
     */
    if (size >= threshold - threshold / 4)
        resize();
}

/**
 * 扩容,扩大为原来的2倍(这样保证了长度为2的冥)
 */
private void resize() {
    ThreadLocal.ThreadLocalMap.Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    ThreadLocal.ThreadLocalMap.Entry[] newTab = new ThreadLocal.ThreadLocalMap.Entry[newLen];
    int count = 0;

    for (int j = 0; j < oldLen; ++j) {
        ThreadLocal.ThreadLocalMap.Entry e = oldTab[j];
        if (e != null) {
            ThreadLocal<?> k = e.get();
            //虽然做过一次清理,但在扩容的时候可能会又存在key==null的情况。
            if (k == null) {
                //这里试试将e.value设置为null
                e.value = null; // Help the GC
            } else {
                //同样适用线性探测来设置值。
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }

    //设置新的阈值
    setThreshold(newLen);
    size = count;
    table = newTab;
}

/**
 * 全清理,清理所有无效entry
 */
private void expungeStaleEntries() {
    ThreadLocal.ThreadLocalMap.Entry[] tab = table;
    int len = tab.length;
    for (int j = 0; j < len; j++) {
        ThreadLocal.ThreadLocalMap.Entry e = tab[j];
        if (e != null && e.get() == null)
            //使用连续段清理
            expungeStaleEntry(j);
    }
}

<a name="Obla8"></a>
## get方法
先获取到当前线程的ThreadLocalMap实例对象map<br />如果map为null<br />则为当前线程创建一个ThreadLocalMap对象,<br />并在相应bucket插入值 key-null,<br />返回null<br />如果map不为null<br />查找相应的bucket,如果查到的不是,就用线性探测查找,直到找到为止<br />返回对应value
```java
public T get() {
    //获取当前线程,以及当前线程的ThreadLocalMap实例对象map
    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;
        }
    }
    //为空返回初始化值 —— 为当前线程创建一个ThreadLocalMap对象,并放入 {ThreadLocal对象:null}
    return setInitialValue();
}

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

protected T initialValue() {
    return null;
}

ThreadLocalMap.getEntry方法

private Entry getEntry(ThreadLocal<?> key) {
    根据key计算索引
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    //如果索引位置的对象不是要找的key
    else
        return getEntryAfterMiss(key, i, e);
}

//直接计算出来的key的下标存储的并非是key时调用此方法
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            //清除无效的entry
            expungeStaleEntry(i);
        else
            基于线性探测法向后查找
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}


remove方法

获取当前现成的ThreadLocalMap对象m
如果m为null,则什么都不做
如果m不为null,则找到对应key

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}


ThreadLocalMap.remove方法

private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    // 计算索引
    int i = key.threadLocalHashCode & (len-1);
    // 进行线性探测,查找正确的KEY
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            //调用weakreference的clear()清除引用
            e.clear();
            //连续段清除
            expungeStaleEntry(i);
            return;
        }
    }
}

使用场景

1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
2、线程间数据隔离
3、进行事务操作,用于存储线程事务信息。
4、数据库连接,Session会话管理。

参考文献

https://www.jianshu.com/p/80866ca6c424