ThreadLocal

Posted lovezmc

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ThreadLocal相关的知识,希望对你有一定的参考价值。

一、原理

ThreadLocal的原理就是将参数放在当前线程中,达到线程隔离的目的。

二、代码分析

1、 ThreadLocalMap

1.1 基础属性

// 默认容量
private static final int INITIAL_CAPACITY = 16;

// ThreadLocalMap底层就是Entry数组,跟HashMap挺像的
private Entry[] table;

// 实际的entry个数
private int size = 0;

// 阈值,大于该值则扩容
private int threshold;

1.2 nextIndex、prevIndex

  这里将Entry数组作为环形,数组最后一个值的下一个值为数组的第一个值。

// len = table.length
private
static int nextIndex(int i, int len) return ((i + 1 < len) ? i + 1 : 0); // len = table.length private static int prevIndex(int i, int len) return ((i - 1 >= 0) ? i - 1 : len - 1);

1.3 set方法

  将<ThreadLocal, Object>放进Entry数组中,并清除key为null的数据(这里涉及到弱引用问题,请转到  查看),最后,根据当前容量判断是否需要扩容。

  注意,这里数组寻址方式和HashMap一样,都是 hashCode & (len-1),不同的是,如果出现hash碰撞,HashMap是将其放在同一个数组里,使用链表或红黑树存储;而这里出现hash碰撞时,则往后查找,找到一个空位将数据放进去。

private void set(ThreadLocal<?> key, Object value) 
    // 利用hashCode寻址
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    // 找到空位后跳出for循环
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) 
        ThreadLocal<?> k = e.get();

        if (k == key) 
            e.value = value;
            return;
        

        if (k == null) 
            // 去除失效的entry,并跳出循环(因为有坑空出来了)
            replaceStaleEntry(key, value, i);
            return;
        
    

    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 如果清理成功,就有新的空间空出来,就不需要扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();


// 清除失效数据,
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) 
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    // 从当前失效的Entry往前找,直至找到空位,让slotToExpunge等于该空位后第一个失效的Entry的下标
    // 原因:每次删除的时两个null值之间的失效值
    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,直接替换即可
        if (k == key) 
            e.value = value;

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

            // 因为这里做了替换,故:当起始删除下标=输入下标时,需要让起始输入下标=替换后的下标
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            // 清除失效数据
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        

        // 如果两个null期间还有其他失效值,让slotToExpunge等于它
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    

    // 删除失效值
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // 如果为true,说明两个null之间除了staleSlot位置,还有其他位置有失效数据,要清理
    // 这也是为什么前面找到了失效值,就让slotToExpunge等于它
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);


// 用于扫描控制:如果没有遇到脏entry就整个扫描过程持续log2(n)次,log2(n)的得来是因为 n>>>=1
// 如果在扫描过程中遇到脏entry的话就会令n为当前hash表的长度(n=len),再扫描log2(n)趟
// n增加是为了增加循环次数从而通过nextIndex往后搜索的范围扩大
private boolean cleanSomeSlots(int i, int n) 
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do 
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) 
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
        
     while ( (n >>>= 1) != 0);
    return removed;


private void rehash() 
    // 先清理失效数据
    expungeStaleEntries();

    // 因为最开始的threshold = len*2/3,这里判断下,如果大于 3/4的threshold再扩容
    // 也是为了更少的扩容操作
    if (size >= threshold - threshold / 4)
        resize();


// 清理失效数据
private void expungeStaleEntries() 
    Entry[] tab = table;
    int len = tab.length;
    for (int j = 0; j < len; j++) 
        Entry e = tab[j];
        if (e != null && e.get() == null)
            expungeStaleEntry(j);
    


// 删除失效Entry和下一个坑之间的失效Entry,返回下一个为null的下标
private int expungeStaleEntry(int staleSlot) 
    Entry[] tab = table;
    int len = tab.length;

    // 删除这个失效的Entry
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // 重复获取直至遇到空值
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) 
        ThreadLocal<?> k = e.get();
        if (k == null) 
            e.value = null;
            tab[i] = null;
            size--;
         else 
            int h = k.threadLocalHashCode & (len - 1);
            // 已失效,删除后找一个坑放进去
            if (h != i) 
                tab[i] = null;

                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            
        
    
    return i;


// 双倍扩容,并进行copy
private void resize() 
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;

    for (int j = 0; j < oldLen; ++j) 
        Entry e = oldTab[j];
        if (e != null) 
            ThreadLocal<?> k = e.get();
            if (k == 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;

1.4 getEntry:先寻址,不是它就往后查找

private Entry getEntry(ThreadLocal<?> key) 
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e); //由于其set机制,get时需要往后查找


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) // 失效了,还未删除,删除后i位置为null,故返回的tab[i]为null
            expungeStaleEntry(i);//清理失效数据
        else
            i = nextIndex(i, len);
        e = tab[i];
    
    return null;//如果e是null的话,直接返回null,说明被清理掉了或不存在

1.5 remove方法:同样的,删除的同时清理失效数据

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;
        
    

2. ThreadLocalMap.Entry

  这里Entry继承了WeakReference<ThreadLocal<?>>,作为key值的ThreadLocal被定义为弱引用,当GC时会将key值回收,而value一直存在,导致内存泄漏。

  内存泄漏的两种可能:

    1. 除了 弱引用 引用的实例之外,其他实例已被回收,gc后该实例也被回收,key为null,value一直数组持有无法回收

    2. 使用static的ThreadLocal的引用是强引用,该强引用作为key放进ThreadLocalMap,如果不手动删除,该ThreadLocal对象不会被回收(除非线程结束,线程中的ThreadLocalMap也随之消失)

  ThreadLocalMap中的解决内存泄漏的办法:调用 ThreadLocal 内部提供的 set、get 方法时,内部调用内部类 ThreadLocalMap 的 get、set 方法,清除为 key 为 null 的 value

强引用:绝对不回收

软引用:内存不足时回收

弱引用:下一次GC时回收

虚引用:对象的回收不受虚引用影响(可以认为没有引用),唯一的作用就是当对象回收时会收到系统通知

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

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

3. Thread类:Thread类的方法都是通过获取当前线程,拿到线程私有的ThreadLocalMap进行操作的

ThreadLocalMap getMap(Thread t) 
    return t.threadLocals;


public void set(T value) 
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);


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


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

 

以上是关于ThreadLocal的主要内容,如果未能解决你的问题,请参考以下文章

线程的补充

ThreadLocal

ThreadLocal

多线程之ThreadLocal

JDBC: ThreadLocal 类

关于ThreadLocal