ThreadLocal 原理解析

o从名字我们就可以看到ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

首先从使用 ThreadLocal 开始看,源码如下

ThreadLocal<Integer> thi = new ThreadLocal<>();

可以看出来 ThreadLocal 仅仅是 new 出来,再来看看构造函数, 可以看出来构造函数什么都没做,由于 ThreadLocal 本身不继承任何类也没实现接口所以,new 一个 ThreadLocal 的时候什么都没做,只是初始化一个对象。

public ThreadLocal() {
}

那么 ThreadLocal 什么时候初始化存储空间呢。这就要看 set 方法。
主要流程:

  1. 先获取 ThreadLocalMap 对象,
  2. 如果没初始化则初始化
  3. 如果已经初始化则设置值
public void set(T value) {
    Thread t = Thread.currentThread();
    // 获取 map 对象
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 设置值
        map.set(this, value);
    else
        // 初始化
        createMap(t, value);
}

getMap 用于获取线程的 threadLocals 成员变量。

ThreadLocalMap getMap(Thread t) {
    // 获取线程的 threadLocals 成员变量
    return t.threadLocals;
}

线程定义 threadLocals 成员变量代码。线程本身没有初始化该成员变量的方法。ThreadLocalMap 是 ThreadLocal 一个内部静态类,其本身提供类似 Map 的存储结构。

ThreadLocal.ThreadLocalMap threadLocals = null;

所以初始是在 createMap 方法中,可以看出只是 new 一个 ThreadLocalMap 对象,并将当前 ThreadLocal 对象和值当做参数传递过去。

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

继续跟进,看看 ThreadLocalMap 的构造方法

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // new 一个数组,默认大小是 16
    table = new Entry[INITIAL_CAPACITY];
    // 获取 ThreadLocal 的 threadLocalHashCode 对容量取模获取索引位置
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    // 对 ThreadLocal 对象和 value 包装成 Entry 然后放入数组。
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

这里需要注意 Entry 是弱引用对象

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

关于 ThreadLocal 成员变量生成方式 threadLocalHashCode,先来看成员变量定义,由代码可以知道每次都是调用 nextHashCode 获取

private final int threadLocalHashCode = nextHashCode();

nextHashCode 方法由代码可以知道是由 nextHashCode 生成每次加上成员变量HASH_INCREMENT 获得。所以所谓 hashCode 就是一个整数每次原子操作加上一个固定的值。


private static AtomicInteger nextHashCode = new AtomicInteger();

// 1640531527
private static final int HASH_INCREMENT = 0x61c88647;
 
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

再来看看 ThreadLocalMap 的 set 方法。
主流程

  1. 计算槽位
  2. 根据槽位获取对象,如果是统一 ThreadLocal 则修改值,如果 ThreadLocal 为 null 则清理槽位,并设置值。否则就是槽位冲突获取下一个位置知道找到位置
  3. 找到合适位置将值添加进去
  4. 清理操作
  5. 如果条件符合则扩容
private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    
    // 先获取所以位置 Entry ,如果存在则进入 for  循环,每次操作获取当前操作的下一个操作
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        // 获取 Entry 对象中的 ThreadLocal
        ThreadLocal<?> k = e.get();
        // 如果是同一个对象,修改值就直接返回
        if (k == key) {
            e.value = value;
            return;
        }

        // 如果 ThreadLocal 对象已经回收则清理
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    // 找到合适位置,将当前 ThreadLocal 和值包装成 Entry 添加到数组
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 然后清理一些槽位
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        // 重新 hash,扩容
        rehash();
}

先看第一个获取槽位 nextIndex,由代码可以知道获取槽位是一个 ringbuffer 的方式,如果 i 是最后一个位置则返回第一个位置,即 0;

private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

再看看 replaceStaleEntry
主要流程

  1. 向前找看看是否有需要清除的槽位
  2. 向找看看是否有需要清除的槽位,在此过程中如果有同一个 ThreadLocal 则修改值,并清理指定槽位
  3. 将当前 ThreadLocal 对象和 value 赋值到对应槽位
  4. 如果有需要清理槽位,清除掉
/**
 * @param  key ThreadLocal 对象
 * @param  value 需要设置的值
 * @param  staleSlot 槽位
 */
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    int slotToExpunge = staleSlot;
    // 获取槽位前一个操作,如果槽位非 null 进入 for 循环
    for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)){
        // 如果前一个槽位 ThreadLocal 对象是 null,则更新 slotToExpunge 重新循环直到,找到一个没有 Entry 的槽位
        if (e.get() == null)
            slotToExpunge = i;
    }
    
    // 获取staleSlot 下一个槽位如果 Entry 非 null 则进入 for 循环
    for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
        // 获取当前槽位 ThreadLocal 对象
        ThreadLocal<?> k = e.get();
        // 如果 ThreadLocal 一致 则修改 value
        if (k == key) {
            e.value = value;
            // 修改值
            tab[i] = tab[staleSlot];
            // 将 staleSlot 槽位赋值
            tab[staleSlot] = e;
            
            // 如果清除槽位和入参一致,则修改清除槽位值
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            // 清理一些槽位
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // 如果 ThreadLocal 是 null, 且清楚槽位还没修改,则修改。
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // 清除槽位的 value
    tab[staleSlot].value = null;
    // 重新赋值槽位 Entry 对象
    tab[staleSlot] = new Entry(key, value);

    // 如果运行中还有其他任何陈旧条目,则将它们清除
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

prevIndex 方法,获取槽位采用 ringbuffer 方式从后向前获取,如果当前槽位是第一个槽位则取最后槽位。

private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}

expungeStaleEntry 清除指定槽位 Entry 对象

private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // 清除 value 和 Entry 对象
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // 重新哈希直到遇到null
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        // 前一个槽位 ThredLocal 是 null 则清除掉
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            // 非 null
            int h = k.threadLocalHashCode & (len - 1);
            // 重新哈希后槽位变了
            if (h != i) {
                // 清除原先槽位
                tab[i] = null;
                // 向后获取槽位直到为 null
                while (tab[h] != null)
                    h = nextIndex(h, len);
                // 重新设定值
                tab[h] = e;
            }
        }
    }
    return i;
}

cleanSomeSlots 清除一些槽位

/**
 * @param i 槽位
 * @param n 槽位总数
 * 
 * @return 如果有有被清除的槽位返回 true
 */
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];
        // ThreadLocal 是 null 则清除
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);
    
    return removed;
}

现在看看 ThreadLocal 的 get 方法,其主流程如下

  1. 获取 ThreadLocalMap 对象
  2. 获取值
  3. 没有初始化则初始化,并返回 null
public T get() {
    Thread t = Thread.currentThread();
    // 获取 ThreadLocalMap 对象
    ThreadLocalMap map = getMap(t);
    // 获取值
    if (map != null) {
        // 获取 Entry 对象
        ThreadLocalMap.Entry e = map.getEntry(this);
        // Entry 对象非 null 则获取其中 value
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    
    // 没有初始化则初始化,并返回 null
    return setInitialValue();
}

setInitialValue 负责初始化,这种情况就是直接设置 null 值。

private T setInitialValue() {
    // 获取 null 值
    T value = initialValue();
    Thread t = Thread.currentThread();
    // 获取 ThreadLocalMap 对象
    ThreadLocalMap map = getMap(t);
    // 已经创建则设置 null
    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) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    // 如果取模的槽位就是当前 ThreadLocal 对象则直接返回
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

取模对应槽位的 Entry 对应 ThreadLocal 不符合则通过 getEntryAfterMiss 获取,主流程如下

  1. Entry 对象的 ThreadLocal 对象如果一直则返回
  2. 如果 ThreadLocal 是空则清除当前槽位
  3. 否则获取下一个槽位
  4. 找不到则返回 null
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)
            // 清除当前槽位
            expungeStaleEntry(i);
        else
            // 获取下一个槽位
            i = nextIndex(i, len);
        e = tab[i];
    }
    
    // 找不到返回 null
    return null;
}

ThreadLocal 如果避免内存泄漏必须 remove,由代码可以知道调用的是 ThreadLocalMap 的 remove 方法。

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);
    
    // 取模槽位对应 Entry 对象非 null 才删除
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        // ThreadLocal 对象一致才删除
        if (e.get() == key) {
            // 清除弱引用指向的 ThreadLocal 对象
            e.clear();
            // 清除槽位
            expungeStaleEntry(i);
            return;
        }
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342