ThreadLocal源码分析

原创
09/23 15:09
阅读数 3.9K

1.ThreadLocal简介

ThreadLocal是线程本地变量,ThreadLocal为每一个线程创建一个单独的变量副本ThreadLocalMap,所以每个线程修改自己变量副本不会影响其它的线程。区别于线程同步,我们知道线程同步是为了解决多线程下共享变量的安全问题,而ThreadLocal是为了解决线程内部数据传递问题。一个线程内部可以有多个ThreadLocal,但是它门维护线程的同一个ThreadLocalMap变量,共用同一个Entry数组。

ThreadLocal数据结构:

每个线程内部有一个ThreadLocalMap属性,ThreadLocal通过维护该属性来保证单个线程内部数据共享。ThreadLocalMap内部有一个entry数组,该数组是key,value型结构,key为当前ThreadLocal的弱引用,value用于存放具体的值,类型为一个泛型结构,支持各种数据变量。ThredLocalMap内Entry数组的下标值也是通过 key.threadLocalHashCode & (数组长度 - 1)来确定的,只不过这个threadLocalHashCode 是通过AutomicLong每次递增0x61c88647来确定的,这可以尽量减少hash碰撞。不同于HashMap,ThreadLocalMap内部只维护了一个Entry数组,所以当发生hash冲突的时候,ThreadLocalMap会将发生hash冲突的Entry放在当前key对应数组下标后面第一个为空的数组槽位内。ThreadLocal的扩容阈值默认为数组大小的 2/3。因为Entry的key为当前threadlcoal的弱引用,所以在发生gc的时候容易导致key被回收,但是此时value为强引用,所以这种情况会导致内存溢出。但是,当我们调用threadlocal的set,get,remove方法的时候,ThreadLocalMap内都会发生回收过期key的操作,不过这种回收是一种抽样回收,可能并不能回收所有的过期key。而且在执行set方法回收的时候,可能发生扩容,这时候的扩容判断是当前数组的长度的1/2。Entry数组默认初始化长度为16。

2.ThreadLocal简单示例

public class ThreadLocalTest {

    private static final ThreadLocal<String> threadLocal = new ThreadLocal();

    private static String str = null;

    public static void print1() {
        System.out.println("打印方法1输出:" + threadLocal.get());
    }

    public static void print2() {
        System.out.println("打印方法2输出:" + str);
    }

    public static void main(String[] args) {
        //线程1
        new Thread(() -> {
            threadLocal.set("线程1设置的str1");
            str = "线程1设置的str2";
            //睡5秒钟
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //睡5秒钟后打印,此时第2个线程早已执行完
            print1();
            print2();
        }).start();

        //线程2
        new Thread(() -> {
            threadLocal.set("线程2设置的str1");
            str = "线程2设置的str2";
            //直接打印
            print1();
            print2();
        }).start();
    }
}

运行结果:

打印方法1输出:线程2设置的str1
打印方法2输出:线程2设置的str2
打印方法1输出:线程1设置的str1
打印方法2输出:线程2设置的str2

根据运行结果分析出,使用ThreadLocal的存储的变量在多线程不存在线程安全问题,常规创建的属性在多线程下存在线程安全问题。

3.ThreadLocal源码分析

3.1.ThreadLocal的属性分析

ThreadLocal中使用了斐波那契散列法,来保证哈希表的离散度。可以保证 nextHashCode 生成的哈希值,均匀的分布在 2 的幂次方上。具体的数学问题不在这里深究。

private final int threadLocalHashCode = nextHashCode();

private static AtomicInteger nextHashCode =
    new AtomicInteger();

//十进制1640531527=0.618*2^32,这个值是黄金分割率*2^32
private static final int HASH_INCREMENT = 0x61c88647;

//每次调用该方法,hashcode值就会递增HASH_INCREMENT
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}
//用于计算数组下标的值,table.length - 1转二进制有N个1,那么
//key.threadLocalHashCode & (table.length - 1)的值就是threadLocalHashCode的低N位
int i = key.threadLocalHashCode & (table.length - 1);

4.ThreadLocal.set方法分析

public void set(T value) {
    //获取当前线程
    Thread t = Thread.currentThread();
    //根据当前线程获取ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    //如果map为空则创建一个,否则设置属性值
    if (map != null)
        //key为当前thread的引用则设置该值
        map.set(this, value);
    else
        //map为空则创建当前线程的ThreadMap并和当前线程绑定
        createMap(t, value);
}

4.1.ThreadLocalMap.set方法分析

private void set(ThreadLocal<?> key, Object value) {
    //将初始化后的当前数组赋值给临时数组tab
    Entry[] tab = table;
    //获取当前临时tab数组长度
    int len = tab.length;
    //计算当前key对应的数组下标
    int i = key.threadLocalHashCode & (len-1);

    //从当前下标开始循环往后遍历,如果当前数组槽为空,则直接跳出循环,如果不为空,则进行key的判断
    //因为ThreadLocalMap的结构只是数组,没有链表,当key发生冲突,
    //不同的key定位到相同的数组下标的时候,会往后寻找第一个下标为null
    //的槽或者第一个key位过期key的槽,并将entry放入并赋值
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        //对应下标为i的槽位为空的时候才会走到循环里面的逻辑
        //获取key
        ThreadLocal<?> k = e.get();
        //CASE1:如果key相同,替换value并跳出循环
        if (k == key) {
            e.value = value;
            return;
        }
        //CASE2:如果key为空,说明key已经过期了,当前下标对应的槽可以被使用
        if (k == null) {
            //替换过期key的逻辑
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    //如果当前下标下的数组槽为空,占用该槽位并赋值
    tab[i] = new Entry(key, value);
    //递增数组大小
    int sz = ++size;
    //没有清理到数据,且size大小达到了扩容阈值
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

4.2.ThreadLocalMap.replaceStaleEntry方法分析

给当前key找数组槽位的时候,找到的下标对应的key为过期的key的时候,执行替换操作

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
    //数组列表
    Entry[] tab = table;
    //数组长度
    int len = tab.length;
    //临时变量
    Entry e;
    //需要清理的数据的开始下标,默认为当前staleSlot
    int slotToExpunge = staleSlot;
    //从当前staleSlot向前查找,找对应数组槽下的entry,直到碰到空的槽则退出循环
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        //如果在查找过程中,碰到key为过期key的情况,更新需要清理的数据的开始下标
        if (e.get() == null)
            slotToExpunge = i;

    //从当前staleSlot向后查找,找对应数组槽下的entry,直到碰到空的槽则退出循环
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        //获取当前元素的key
        ThreadLocal<?> k = e.get();

        //如果key相同,则替换value,迁移数据位置
        if (k == key {
            e.value = value;
            //将过期的tab[staleSlot]放到找到的i下标下
            tab[i] = tab[staleSlot];
            //当前staleSlot下标下的槽替换为当前的entry,数据的位置被优化了
            tab[staleSlot] = e;

            //条件成立说明向前过程中并没有找到过期的key
            if (slotToExpunge == staleSlot)
                //修改需要清理数据的开始下标为替换数据后的下标
                slotToExpunge = i;
            //清理数据
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }
        
        //k==null说明循环过程中未找到匹配的key
        //slotToExpunge == staleSlot说明向前遍历过程中未找到过期的key
        if (k == null && slotToExpunge == staleSlot)
            //可以将循环向后查找的i指向slotToExpunge,因为在向后查找的过程中没有找到相同的key
            //该段期间没必要处理了
            slotToExpunge = i;
    }

    //走到这里说明循环向后查找的过程中,没有找到相同的key
    //直接使用当前下标并赋值        
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

     //条件成立,说明在向前向后遍历中,slotToExpunge被改变了  
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

4.3.ThreadLocalMap.cleanSomeSlots方法分析

为什么有while ( (n >>>= 1) != 0),这样不是可能清理不了所有数据吗?是的,ThreadLocal的设计行就是部分清除,类似于抽样,避免清理所有影响性能。

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

4.4.ThreadLocalMap.rehash扩容操作

扩容之前,进行一次全面的清理操作

private void rehash() {
    expungeStaleEntries();

    if (size >= threshold - threshold / 4)
        resize();
}

扩容逻辑,比较简单,数组变大两倍,旧数据迁移到新数组,如果key已经过期的,则直接将value也设置为空。这里需要注意的时候,清理过程中扩容的阈值是原数组容量的 1/2, size >= threshold - threshold / 4,我们直到threashold = 2 / 3 * length, 所以转化后size >= 3 / 4 * (2 / 3) * length。

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();
            //如果对应的key已经回收
            if (k == null) {
                //value设置为空
                e.value = null; // Help the GC
            } else {
                //进行数据迁移,如果存在冲突,则放到计算出来的下标的后方第一个不为null的槽
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }
    //重新设置扩容阈值
    setThreshold(newLen);
    size = count;
    table = newTab;
}

5.ThreadLocal.get方法分析

5.1.方法调用过程

当我们调用threadLocal的get方法的时候,首先会调用getMap方法,该方法根据当前线程获取当前线程的ThreadLocal.ThreadLocalMap threadLocals属性,如果非空,再获取对应的ThreadLocal的ThreadLocalMap 里面的entry,根据entry获取对应的value,这个过程会调用expungestaleEntry方法,清空key为空的hash槽的值,并将key不为空的且通过key的hash值计算出来的下标发生过向后偏移的entry移动到更靠近计算出来的下标值的后面的某个空的槽内。如果getMap返回空,说明我们可能没用调用ThreadLocal的set方法的情况下调用了get方法,那么创建一个ThreadLocalMap,初始化entry数组,设置扩容阈值,并设置对应的ThreadLocal的hash槽的值为空。

 

public T get() {
    //获取当前线程
    Thread t = Thread.currentThread();
    //取出当前线程的ThreadLocalMap属性
    ThreadLocalMap map = getMap(t);
    //如果当前线程的ThreadLocalMap不为空
    if (map != null) {
        //获取ThreadLocalMap的Entry数组
        ThreadLocalMap.Entry e = map.getEntry(this);
        //如果数组不为空,取出value值返回
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

5.2.ThreadLocal.getMap方法分析

//获取thread的threadLocals属性
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

5.3.ThreadLocalMap.getEntry方法分析

//获取ThreadLocalMap的entry数组对应下标的数据
private Entry getEntry(ThreadLocal<?> key) {
    //计算下标
    int i = key.threadLocalHashCode & (table.length - 1);
    //获取对应下标数据
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    //如果取不到,为什么有这种情况?
    //从put方法中我们知道,threadlocalMap不同于hashMap
    //内部只有数组,数组的每个hash槽下只有一个entry值
    //如果在put的时候发现对应hash槽的值不为空,且key不相同
    //则往后找第一个为空的hash槽,讲entry放入该hash槽
    else
        return getEntryAfterMiss(key, i, e);
}

5.4.ThreadLocalMap.getEntryAfterMiss方法分析

//从对应下标往后循环查找,这里有个特殊的地方nextIndex
//该方法:从对应下标往后循环返回下标,如果超出数组长度,
//则从0下标开始继续往后循环返回下标
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    //循环遍历
    while (e != null) {
        ThreadLocal<?> k = e.get();
        //case1:key值相同,返回对应的entry
        if (k == key)
            return e;
        //case2:发现对应entry数组下标下的key为空,清理
        if (k == null)
            expungeStaleEntry(i);
        //case3:key不为空但key不相同,数组下标往后推进  
        else
            i = nextIndex(i, len);
        //返回下一个下标值对应的entry
        e = tab[i];
    }
    return null;
}
//从对应下标往后循环,如果超出数组长度,则从0下标开始继续往后循环
//返回具体下标值
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

5.5.ThreadLocalMap.expungeStaleEntry方法分析

从当前staleSlot开始循环清理过期key对应的entry数组内的值;如果key不为空且当前线程对应的threadlocal的hash值计算出来的下标发生过迁移,说明之前在put的时候,在对应下标下发生过hash冲突,将当前下标下的entry数组对应的值置为null,并将当前下标下的entry值移动到更接近通过hash值计算出来的下标之后的某个空的槽中。循环在进行下标右移的过程中,如果碰到对应下标下的槽数据为空,则退出循环。该方法在执行的时候会将本该在staleSlot位置的key对应的变量移动到该位置或更靠近该位置的后方。避免remove方法遍历的时候出现null导致清理不到的情况。

private int expungeStaleEntry(int staleSlot) {
    //将全局entry数组赋值给临时tab
    Entry[] tab = table;
    //临时entry数组当前长度
    int len = tab.length;

    //设置对应数组下标下的entry的value为空
    tab[staleSlot].value = null;
    //设置对应entry为空
    tab[staleSlot] = null;
    //entry数组全局长度-1
    size--;

    Entry e;
    int i;
    //从当前下标往后循环遍历,直到对应的下标下槽内数据为空跳出循环
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        //获取对应下标下当前entry对应的key
        ThreadLocal<?> k = e.get();
        //如果key为空则清理entry的value和设置当前数组对应entry为空
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        //如果key不为空
        } else {
            //计算获取对应的下标,这个本该是存放entry的位置,但是可能由于hash冲突,put的时候向后偏移了
            int h = k.threadLocalHashCode & (len - 1);
            //条件成立说明在put的时候计算出来的下标发生过hash冲突
            //数据向后偏移过,而且 h < i
            if (h != i) {
                //将当前下标下entry设置为空
                tab[i] = null;

                //从计算出来的下标h循环向后获取一个对应entry为空的下标值
                //该下标下存放当前entry
                while (tab[h] != null)
                    //这个新计算出来的h的值更靠近计算获取的下标
                    h = nextIndex(h, len);
                //将entry放在对应下标
                tab[h] = e;
            }
        }
    //返回进行处理过后的起点下标i
    return i;
}

5.6.ThreadLocal.setInitialValue方法分析

private T setInitialValue() {
    //获取一个空值
    T value = initialValue();
    Thread t = Thread.currentThread();
    //获取当前线程的ThreadMap
    ThreadLocalMap map = getMap(t);
    //如果不为空,则将当前空值注入
    if (map != null)
        map.set(this, value);
    else
        //否则创建这个ThreadMap并和当前Thread绑定
        createMap(t, value);
    return value;
}

6.ThreadLocal.remove方法分析

remove方法也很简单,就是将key的引用设置为null,然后找到key所对应的数组槽位,执行清理操作。

在ThreadLocal使用完毕后,执行remove方法防止内存溢出。

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}
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;
        }
    }
}
public void clear() {
    this.referent = null;
}

7.InheritableThreadLocal分析

上面说完了ThreadLocal的问题,可以看出,ThreadLocal只能在单个线程内部传递参数,无法在子父线程间传递参数。

但是InheritableThreadLocal的出现解决了这个问题。

public class InheriTableThreadLocalTest {

    private static final InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) {
        threadLocal.set("主线程设置值");

        new Thread(() -> {
            System.out.println(threadLocal.get());
        }).start();
    }
}

分析InheritableThreadLocal类,发现继承于ThreadLocal,但是在createMap,getMap的时候维护的是inheritableThreadLocals

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    protected T childValue(T parentValue) {
        return parentValue;
    }

    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

在线程初始化的代码init方法中,有这么一段逻辑:

如果父线程的inheritThreadLocals不为空,则调用ThreadLocal.createInheritedMap方法,该方法传递了父线程的inheritableThreadLocals

if (inheritThreadLocals && parent.inheritableThreadLocals != null)
    this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

再看看ThreadLocal.createInheritedMap方法,子线程在创建的时候,将父线程的inheritableThreadLocals复制了过来保存在了自己的inheritableThreadLocals中。

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}
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++;
            }
        }
    }
}

8.推荐

分享一个朋友的公众号,有很多干货,包含netty,spring,线程,spring cloud等详细讲解,也有详细的学习规划图,面试题整理等,我感觉在讲课这块比我讲的清楚:

展开阅读全文
打赏
1
7 收藏
分享
加载中
更多评论
打赏
0 评论
7 收藏
1
分享
返回顶部
顶部