资深架构师浅析 ThreadLocal 源码 - 黄金分割数的使用 - 今日头条

本文由 简悦 SimpRead 转码, 原文地址 www.toutiao.com

二. 黄金分割数与斐波那契数列首先复习一下斐波那契数列,下面的推导过程来自某搜索引擎的 wiki:斐波那契数列:1, 1, 2, 3, 5, 8,

来源: https://www.codercto.com/a/92268.html

一. 前提

最近接触到的一个项目要兼容新老系统,最终采用了 ThreadLocal(实际上用的是 InheritableThreadLocal) 用于在子线程获取父线程中共享的变量。问题是解决了,但是后来发现对 ThreadLocal 的理解不够深入,于是顺便把它的源码阅读理解了一遍。在谈到 ThreadLocal 之前先买个关子,先谈谈黄金分割数。本文在阅读 ThreadLocal 源码的时候是使用 JDK8(1.8.0_181)。

https://p3.toutiaoimg.com/origin/pgc-image/b60fca069f864daf90c5bfd3b989b585?from=pc

二. 黄金分割数与斐波那契数列

首先复习一下斐波那契数列,下面的推导过程来自某搜索引擎的 wiki:

  • 斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …
  • 通项公式:假设 F(n) 为该数列的第 n 项(n ∈ N*),那么这句话可以写成如下形式:F(n) = F(n-1) + F(n-2)。

有趣的是,这样一个完全是自然数的数列,通项公式却是用无理数来表达的。而且当 n 趋向于无穷大时,前一项与后一项的比值越来越逼近 0.618(或者说后一项与前一项的比值小数部分越来越逼近 0.618),而这个值 0.618 就被称为黄金分割数。证明过程如下:

https://p3.toutiaoimg.com/origin/pgc-image/11e14601a280454c92cf2fe3007ff775?from=pc

黄金分割数的准确值为 (根号 5 - 1)/2,约等于 0.618。

三. 黄金分割数的应用

黄金分割数被广泛使用在美术、摄影等艺术领域,因为它具有严格的比例性、艺术性、和谐性,蕴藏着丰富的美学价值,能够激发人的美感。当然,这些不是本文研究的方向,我们先尝试求出无符号整型和带符号整型的黄金分割数的具体值:

1
2
3
4
5
6
7
8
public static void main(String[] args) throws Exception {
    //黄金分割数 * 2的32次方 = 2654435769 - 这个是无符号32位整数的黄金分割数对应的那个值
    long c = (long) ((1L << 32) * (Math.sqrt(5) - 1) / 2);
    System.out.println(c);
    //强制转换为带符号为的32位整型,值为-1640531527
    int i = (int) c;
    System.out.println(i);
}

通过一个线段图理解一下:

https://p3.toutiaoimg.com/origin/pgc-image/3f1f0e615c0d47d291c734a5b2748052?from=pc

也就是 2654435769 为 32 位无符号整数的黄金分割值,而 - 1640531527 就是 32 位带符号整数的黄金分割值。而 ThreadLocal 中的哈希魔数正是 1640531527(十六进制为 0x61c88647)。为什么要使用 0x61c88647 作为哈希魔数?这里提前说一下 ThreadLocal 在 ThreadLocalMap(ThreadLocal 在 ThreadLocalMap 以 Key 的形式存在) 中的哈希求 Key 下标的规则:

哈希算法:keyIndex = ((i + 1) * HASH_INCREMENT) & (length - 1)

其中,i 为 ThreadLocal 实例的个数,这里的 HASH_INCREMENT 就是哈希魔数 0x61c88647,length 为 ThreadLocalMap 中可容纳的 Entry(K-V 结构) 的个数 (或者称为容量)。在 ThreadLocal 中的内部类 ThreadLocalMap 的初始化容量为 16,扩容后总是 2 的幂次方,因此我们可以写个 Demo 模拟整个哈希的过程:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class Main {
    
    private static final int HASH_INCREMENT = 0x61c88647;

    public static void main(String[] args) throws Exception {
        hashCode(4);
        hashCode(16);
        hashCode(32);
    }

    private static void hashCode(int capacity) throws Exception {
        int keyIndex;
        for (int i = 0; i < capacity; i++) {
            keyIndex = ((i + 1) * HASH_INCREMENT) & (capacity - 1);
            System.out.print(keyIndex);
            System.out.print(" ");
        }
        System.out.println();
    }
}

上面的例子中,我们分别模拟了 ThreadLocalMap 容量为 4,16,32 的情况下,不触发扩容,并且分别” 放入”4,16,32 个元素到容器中,输出结果如下:

每组的元素经过散列算法后恰好填充满了整个容器,也就是实现了完美散列。实际上,这个并不是偶然,其实整个哈希算法可以转换为多项式证明:证明 (x - y) HASH_INCREMENT != 2^n (n m),在 x != y,n != m,HASH_INCREMENT 为奇数的情况下恒成立,具体证明可以自行完成。HASH_INCREMENT 赋值为 0x61c88647 的 API 文档注释如下:

连续生成的哈希码之间的差异 (增量值),将隐式顺序线程本地 id 转换为几乎最佳分布的乘法哈希值,这些不同的哈希值最终生成一个 2 的幂次方的哈希表。

四. ThreadLocal 是什么

下面引用 ThreadLocal 的 API 注释:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. 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)

稍微翻译一下:ThreadLocal 提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问 ThreadLocal 实例的时候(通过其 get 或 set 方法)都有自己的、独立初始化的变量副本。ThreadLocal 实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户 ID 或事务 ID)与线程关联起来。

ThreadLocal 由 Java 界的两个大师级的作者编写,Josh Bloch 和 Doug Lea。Josh Bloch 是 JDK5 语言增强、Java 集合 (Collection) 框架的创办人以及《Effective Java》系列的作者。Doug Lea 是 JUC(java.util.concurrent)包的作者,Java 并发编程的泰斗。所以,ThreadLocal 的源码十分值得学习。

五. ThreadLocal 的原理

ThreadLocal 虽然叫线程本地 (局部) 变量,但是实际上它并不存放任何的信息,可以这样理解:它是线程 (Thread) 操作 ThreadLocalMap 中存放的变量的桥梁。它主要提供了初始化、set()、get()、remove()几个方法。这样说可能有点抽象,下面画个图说明一下在线程中使用 ThreadLocal 实例的 set()和 get()方法的简单流程图。

假设我们有如下的代码,主线程的线程名字是 main(也有可能不是 main):

1
2
3
4
5
6
7
8
9
public class Main {
    
    private static final ThreadLocal<String> LOCAL = new ThreadLocal<>();
    
    public static void main(String[] args) throws Exception{
        LOCAL.set("doge");
        System.out.println(LOCAL.get());
    }
}

https://p3.toutiaoimg.com/origin/pgc-image/8ce637aeea694c289336f2b37960104a?from=pc

上面只描述了单线程的情况并且因为是主线程忽略了 Thread t = new Thread() 这一步,如果有多个线程会稍微复杂一些,但是原理是不变的,ThreadLocal 实例总是通过 Thread.currentThread() 获取到当前操作线程实例,然后去操作线程实例中的 ThreadLocalMap 类型的成员变量,因此它是一个桥梁,本身不具备存储功能。

六. ThreadLocal 源码分析

对于 ThreadLocal 的源码,我们需要重点关注 set()、get()、remove() 几个方法。

1. ThreadLocal 的内部属性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//获取下一个ThreadLocal实例的哈希魔数
private final int threadLocalHashCode = nextHashCode();

//原子计数器,主要到它被定义为静态
private static AtomicInteger nextHashCode = new AtomicInteger();

//哈希魔数(增长数),也是带符号的32位整型值黄金分割值的取正
private static final int HASH_INCREMENT = 0x61c88647;

//生成下一个哈希魔数
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

这里需要注意一点,threadLocalHashCode 是一个 final 的属性,而原子计数器变量 nextHashCode 和生成下一个哈希魔数的方法 nextHashCode() 是静态变量和静态方法,静态变量只会初始化一次。换而言之,每新建一个 ThreadLocal 实例,它内部的 threadLocalHashCode 就会增加 0x61c88647。举个例子:

1
2
3
4
5
6
//t1中的threadLocalHashCode变量为0x61c88647
ThreadLocal t1 = new ThreadLocal();
//t2中的threadLocalHashCode变量为0x61c88647 + 0x61c88647
ThreadLocal t2 = new ThreadLocal();
//t3中的threadLocalHashCode变量为0x61c88647 + 0x61c88647 + 0x61c88647
ThreadLocal t3 = new ThreadLocal();

threadLocalHashCode 是下面的 ThreadLocalMap 结构中使用的哈希算法的核心变量,对于每个 ThreadLocal 实例,它的 threadLocalHashCode 是唯一的。

2. 内部类 ThreadLocalMap 的基本结构和源码分析

ThreadLocal 内部类 ThreadLocalMap 使用了默认修饰符,也就是包 (包私有) 可访问的。ThreadLocalMap 内部定义了一个静态类 Entry。我们重点看下 ThreadLocalMap 的源码,先看成员和结构部分:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
 * ThreadLocalMap是一个定制的散列映射,仅适用于维护线程本地变量。
 * 它的所有方法都是定义在ThreadLocal类之内。
 * 它是包私有的,所以在Thread类中可以定义ThreadLocalMap作为变量。
 * 为了处理非常大(指的是值)和长时间的用途,哈希表的Key使用了弱引用(WeakReferences)。
 * 引用的队列(弱引用)不再被使用的时候,对应的过期的条目就能通过主动删除移出哈希表。
 */
static class ThreadLocalMap {
 
        //注意这里的Entry的Key为WeakReference<ThreadLocal<?>>
    static class Entry extends WeakReference<ThreadLocal<?>> {
        
        //这个是真正的存放的值
        Object value;
                // Entry的Key就是ThreadLocal实例本身,Value就是输入的值
        Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
                }
    }
        //初始化容量,必须是2的幂次方
    private static final int INITIAL_CAPACITY = 16;

        //哈希(Entry)表,必须时扩容,长度必须为2的幂次方
    private Entry[] table;

        //哈希表中元素(Entry)的个数
    private int size = 0;
 
        //下一次需要扩容的阈值,默认值为0
    private int threshold;
   
        //设置下一次需要扩容的阈值,设置值为输入值len的三分之二
    private void setThreshold(int len) {
        threshold = len * 2 / 3;
    }
    
    // 以len为模增加i
    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }
     
    // 以len为模减少i
    private static int prevIndex(int i, int len) {
        return ((i - 1 >= 0) ? i - 1 : len - 1);
    }
}

这里注意到十分重要的一点:ThreadLocalMap$Entry 是 WeakReference(弱引用),并且键值 Key 为 ThreadLocal 实例本身,这里使用了无限定的泛型通配符。

接着看 ThreadLocalMap 的构造函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 构造ThreadLocal时候使用,对应ThreadLocal的实例方法void createMap(Thread t, T firstValue)
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 哈希表默认容量为16
    table = new Entry[INITIAL_CAPACITY];
    // 计算第一个元素的哈希码
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

// 构造InheritableThreadLocal时候使用,基于父线程的ThreadLocalMap里面的内容进行提取放入新的ThreadLocalMap的哈希表中
// 对应ThreadLocal的静态方法static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap)
private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];
    // 基于父ThreadLocalMap的哈希表进行拷贝
    for (Entry e : parentTable) {
        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++;
            }
        }
    }
}

这里注意一下,ThreadLocal 的 set() 方法调用的时候会懒初始化一个 ThreadLocalMap 并且放入第一个元素。而 ThreadLocalMap 的私有构造是提供给静态方法 ThreadLocal#createInheritedMap() 使用的。

接着看 ThreadLocalMap 提供给 ThreadLocal 使用的一些实例方法:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
// 如果Key在哈希表中找不到哈希槽的时候会调用此方法
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    // 这里会通过nextIndex尝试遍历整个哈希表,如果找到匹配的Key则返回Entry
    // 如果哈希表中存在Key == null的情况,调用expungeStaleEntry进行清理
    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];
    }
    return null;
}

// 1.清空staleSlot对应哈希槽的Key和Value
// 2.对staleSlot到下一个空的哈希槽之间的所有可能冲突的哈希表部分槽进行重哈希,置空Key为null的槽
// 3.注意返回值是staleSlot之后的下一个空的哈希槽的哈希码
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    
    // expunge entry at staleSlot
    // 清空staleSlot对应哈希槽的Key和Value
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    // 下面的过程是对staleSlot到下一个空的哈希槽之间的所有可能冲突的哈希表部分槽进行重哈希,置空Key为null的槽
    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;

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

// 这里个方法比较长,作用是替换哈希码为staleSlot的哈希槽中Entry的值
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    // Back up to check for prior stale entry in current run.
    // We clean out whole runs at a time to avoid continual
    // incremental rehashing due to garbage collector freeing
    // up refs in bunches (i.e., whenever the collector runs).
    int slotToExpunge = staleSlot;
    // 这个循环主要是为了找到staleSlot之前的最前面的一个Key为null的哈希槽的哈希码
    for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

    // Find either the key or trailing null slot of run, whichever
    // occurs first
    // 遍历staleSlot之后的哈希槽,如果Key匹配则用输入值替换
    for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        // If we find key, then we need to swap it
        // with the stale entry to maintain hash table order.
        // The newly stale slot, or any other stale slot
        // encountered above it, can then be sent to expungeStaleEntry
        // to remove or rehash all of the other entries in run.
        if (k == key) {
            e.value = value;
            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;
            // Start expunge at preceding stale entry if it exists
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // If we didn't find stale entry on backward scan, the
        // first stale entry seen while scanning for key is the
        // first still present in the run.
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }
    // Key匹配不了,则新创建一个哈希槽
    // If key not found, put new entry in stale slot
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);
    
    // 这里如果当前的staleSlot和找到前置的slotToExpunge不一致会进行一次清理
    // If there are any other stale entries in run, expunge them
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

// 对当前哈希表中所有的Key为null的Entry调用expungeStaleEntry
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);
    }
}

// 清理第i个哈希槽之后的n个哈希槽,如果遍历的时候发现Entry的Key为null,则n会重置为哈希表的长度,expungeStaleEntry有可能会重哈希使得哈希表长度发生变化
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;
}


/**
 * 这个方法主要给`ThreadLocal#get()`调用,通过当前ThreadLocal实例获取哈希表中对应的Entry
 *
 */
private Entry getEntry(ThreadLocal<?> key) {
    // 计算Entry的哈希值
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i]; 
    if (e != null && e.get() == key)
        return e;
    else  // 注意这里,如果e为null或者Key对不上,会调用getEntryAfterMiss
        return getEntryAfterMiss(key, i, e);
}

// 重哈希,必要时进行扩容
private void rehash() {
    // 清理所有空的哈希槽,并且进行重哈希
    expungeStaleEntries();

    // Use lower threshold for doubling to avoid hysteresis
    // 哈希表的哈希元素个数大于3/4阈值时候触发扩容
    if (size >= threshold - threshold / 4)
        resize();
}

// 扩容,简单的扩大2倍的容量        
private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;

    for (Entry e : oldTab) {
        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;
}

// 基于ThreadLocal作为key,对当前的哈希表设置值,此方法由`ThreadLocal#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;
        }
        // 如果Entry的Key为null,则替换该Key为当前的key,并且设置值
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 清理当前新设置元素的哈希槽下标到sz段的哈希槽,如果清理成功并且sz大于阈值则触发扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

简单来说,ThreadLocalMap 是 ThreadLocal 真正的数据存储容器,实际上 ThreadLocal 数据操作的复杂部分的所有逻辑都在 ThreadLocalMap 中进行,而 ThreadLocalMap 实例是 Thread 的成员变量,在 ThreadLocal#set() 方法首次调用的时候设置到当前执行的线程实例中。如果在同一个线程中使用多个 ThreadLocal 实例,实际上,每个 ThreadLocal 实例对应的是 ThreadLocalMap 的哈希表中的一个哈希槽。举个例子,在主函数主线程中使用多个 ThreadLocal 实例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class ThreadLocalMain {

    private static final ThreadLocal<Integer> TL_1 = new ThreadLocal<>();
    private static final ThreadLocal<String> TL_2 = new ThreadLocal<>();
    private static final ThreadLocal<Long> TL_3 = new ThreadLocal<>();

    public static void main(String[] args) throws Exception {
        TL_1.set(1);
        TL_2.set("1");
        TL_3.set(1L);
        Field field = Thread.class.getDeclaredField("threadLocals");
        field.setAccessible(true);
        Object o = field.get(Thread.currentThread());
        System.out.println(o);
    }
}

实际上,主线程的 threadLocals 属性中的哈希表中一般不止我们上面定义的三个 ThreadLocal,因为加载主线程的时候还有可能在其他地方使用到 ThreadLocal,笔者某次 Debug 的结果如下:

https://p3.toutiaoimg.com/origin/pgc-image/b2e6f6b36798462a9d66f3a90e3176fb?from=pc

用 PPT 画图简化一下:

https://p3.toutiaoimg.com/origin/pgc-image/cbf0e49003be4cfa8f276d7346866365?from=pc

上图 threadLocalHashCode 属性一行的表是为了标出每个 Entry 的哈希槽的哈希值,实际上,threadLocalHashCode 是 ThreadLocal@XXXX 中的一个属性,这是很显然的,本来 threadLocalHashCode 就是 ThreadLocal 的一个成员变量。

上面只是简单粗略对 ThreadLocalMap 的源码进行了流水账的分析,下文会作一些详细的图,说明一下 ThreadLocal 和 ThreadLocalMap 中的一些核心操作的过程。

3. ThreadLocal 的创建

从 ThreadLocal 的构造函数来看,ThreadLocal 实例的构造并不会做任何操作,只是为了得到一个 ThreadLocal 的泛型实例,后续可以把它作为 ThreadLocalMap$Entry 的键:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 注意threadLocalHashCode在每个新`ThreadLocal`实例的构造同时已经确定了
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

// 通过Supplier去覆盖initialValue方法
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
    return new SuppliedThreadLocal<>(supplier);
}

// 默认公有构造函数
public ThreadLocal() {

}

注意 threadLocalHashCode 在每个新 ThreadLocal 实例的构造同时已经确定了,这个值也是 Entry 哈希表的哈希槽绑定的哈希值。

4. TreadLocal 的 set 方法

ThreadLocal 中 set() 方法的源码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void set(T value) {
    //设置值前总是获取当前线程实例
    Thread t = Thread.currentThread();
    //从当前线程实例中获取threadLocals属性
    ThreadLocalMap map = getMap(t);
    if (map != null)
         //threadLocals属性不为null则覆盖key为当前的ThreadLocal实例,值为value
         map.set(this, value);
    else
    //threadLocals属性为null,则创建ThreadLocalMap,第一个项的Key为当前的ThreadLocal实例,值为value
        createMap(t, value);
}

// 这里看到获取ThreadLocalMap实例时候总是从线程实例的成员变量获取
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

// 创建ThreadLocalMap实例的时候,会把新实例赋值到线程实例的threadLocals成员
void createMap(Thread t, T firstValue) {
     t.threadLocals = new ThreadLocalMap(this, firstValue);
}

上面的过程源码很简单,设置值的时候总是先获取当前线程实例并且操作它的变量 threadLocals。步骤是:

  • 获取当前运行线程的实例。
  • 通过线程实例获取线程实例成员 threadLocals(ThreadLocalMap),如果为 null,则创建一个新的 ThreadLocalMap 实例赋值到 threadLocals。
  • 通过 threadLocals 设置值 value,如果原来的哈希槽已经存在值,则进行覆盖。

https://p3.toutiaoimg.com/origin/pgc-image/2f539b7d2a3e48739ca094adccb691ab?from=pc

https://p3.toutiaoimg.com/origin/pgc-image/d8d3299afaa54df8b1f2949f903eb754?from=pc

https://p3.toutiaoimg.com/origin/pgc-image/4daeddc2271649008df8a44ee1be41e9?from=pc

5. TreadLocal 的 get 方法

ThreadLocal 中 get() 方法的源码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public T get() {
   //获取当前线程的实例
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
    //根据当前的ThreadLocal实例获取ThreadLocalMap中的Entry,使用的是ThreadLocalMap的getEntry方法
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T) e.value;
             return result;
            }
        }
    //线程实例中的threadLocals为null,则调用initialValue方法,并且创建ThreadLocalMap赋值到threadLocals
    return setInitialValue();
}

private T setInitialValue() {
    // 调用initialValue方法获取值
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // ThreadLocalMap如果未初始化则进行一次创建,已初始化则直接设置值
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

protected T initialValue() {
     return null;
}

initialValue() 方法默认返回 null,如果 ThreadLocal 实例没有使用过 set() 方法直接使用 get() 方法,那么 ThreadLocalMap 中的此 ThreadLocal 为 Key 的项会把值设置为 initialValue() 方法的返回值。如果想改变这个逻辑可以对 initialValue() 方法进行覆盖。

https://p3.toutiaoimg.com/origin/pgc-image/3e2045c733e34887aa5957a2e52d7973?from=pc

6. TreadLocal 的 remove 方法

ThreadLocal 中 remove() 方法的源码如下:

1
2
3
4
5
6
7
public void remove() {
    //获取Thread实例中的ThreadLocalMap
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
       //根据当前ThreadLocal作为Key对ThreadLocalMap的元素进行移除
       m.remove(this);
}

https://p3.toutiaoimg.com/origin/pgc-image/c3c7fdd151774fcaa69122e51c445b69?from=pc

七. ThreadLocal.ThreadLocalMap 的初始化

我们可以关注一下 java.lang.Thread 类里面的变量:

1
2
3
4
5
6
7
public class Thread implements Runnable {

  //传递ThreadLocal中的ThreadLocalMap变量
  ThreadLocal.ThreadLocalMap threadLocals = null;
  //传递InheritableThreadLocal中的ThreadLocalMap变量
  ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

也就是,ThreadLocal 需要存放和获取的数据实际上绑定在 Thread 实例的成员变量 threadLocals 中,并且是 ThreadLocal#set() 方法调用的时候才进行懒加载的,可以结合上一节的内容理解一下,这里不展开。

八. 什么情况下 ThreadLocal 的使用会导致内存泄漏

其实 ThreadLocal 本身不存放任何的数据,而 ThreadLocal 中的数据实际上是存放在线程实例中,从实际来看是线程内存泄漏,底层来看是 Thread 对象中的成员变量 threadLocals 持有大量的 K-V 结构,并且线程一直处于活跃状态导致变量 threadLocals 无法释放被回收。threadLocals 持有大量的 K-V 结构这一点的前提是要存在大量的 ThreadLocal 实例的定义,一般来说,一个应用不可能定义大量的 ThreadLocal,所以一般的泄漏源是线程一直处于活跃状态导致变量 threadLocals 无法释放被回收。但是我们知道,·ThreadLocalMap· 中的 Entry 结构的 Key 用到了弱引用 (·WeakReference<ThreadLocal>·),当没有强引用来引用 ThreadLocal 实例的时候,JVM 的 GC 会回收 ThreadLocalMap 中的这些 Key,此时,ThreadLocalMap 中会出现一些 Key 为 null,但是 Value 不为 null 的 Entry 项,这些 Entry 项如果不主动清理,就会一直驻留在 ThreadLocalMap 中。也就是为什么 ThreadLocal 中 get()、set()、remove() 这些方法中都存在清理 ThreadLocalMap 实例 key 为 null 的代码块。总结下来,内存泄漏可能出现的地方是:

  • 大量地 (静态) 初始化 ThreadLocal 实例,初始化之后不再调用 get()、set()、remove()方法。
  • 初始化了大量的 ThreadLocal,这些 ThreadLocal 中存放了容量大的 Value,并且使用了这些 ThreadLocal 实例的线程一直处于活跃的状态。

ThreadLocal 中一个设计亮点是 ThreadLocalMap 中的 Entry 结构的 Key 用到了弱引用。试想如果使用强引用,等于 ThreadLocalMap 中的所有数据都是与 Thread 的生命周期绑定,这样很容易出现因为大量线程持续活跃导致的内存泄漏。使用了弱引用的话,JVM 触发 GC 回收弱引用后,ThreadLocal 在下一次调用 get()、set()、remove() 方法就可以删除那些 ThreadLocalMap 中 Key 为 null 的值,起到了惰性删除释放内存的作用。

其实 ThreadLocal 在设置内部类 ThreadLocal.ThreadLocalMap 中构建的 Entry 哈希表已经考虑到内存泄漏的问题,所以 ThreadLocal.ThreadLocalMap$Entry 类设计为弱引用,类签名为 static class Entry extends WeakReference<ThreadLocal>。之前一篇文章介绍过,如果弱引用关联的对象如果置为 null,那么该弱引用会在下一次 GC 时候回收弱引用关联的对象。举个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class ThreadLocalMain {

    private static ThreadLocal<Integer> TL_1 = new ThreadLocal<>();

    public static void main(String[] args) throws Exception {
        TL_1.set(1);
        TL_1 = null;
        System.gc();
        Thread.sleep(300);
    }
}

这种情况下,TL_1 这个 ThreadLocal 在主动 GC 之后,线程绑定的 ThreadLocal.ThreadLocalMap 实例中的 Entry 哈希表中原来的 TL_1 所在的哈希槽 Entry 的引用持有值 referent(继承自 WeakReference) 会变成 null,但是 Entry 中的 value 是强引用,还存放着 TL_1 这个 ThreadLocal 未回收之前的值。这些被” 孤立” 的哈希槽 Entry 就是前面说到的要惰性删除的哈希槽。

九. ThreadLocal 的最佳实践

其实 ThreadLocal 的最佳实践很简单:

  • 每次使用完 ThreadLocal 实例,都调用它的 remove() 方法,清除 Entry 中的数据。

调用 remove() 方法最佳时机是线程运行结束之前的 finally 代码块中调用,这样能完全避免操作不当导致的内存泄漏,这种主动清理的方式比惰性删除有效。

十. 小结

ThreadLocal 线程本地变量是线程实例传递和存储共享变量的桥梁,真正的共享变量还是存放在线程实例本身的属性中。ThreadLocal 里面的基本逻辑并不复杂,但是一旦涉及到性能影响、内存回收 (弱引用) 和惰性删除等环节,其实它考虑到的东西还是相对全面而且有效的。

写在最后

  • 第一:看完点赞,感谢您的认可;
  • 第二:随手转发,分享知识,让更多人学习到;
  • 第三:记得点关注,每天更新的!!!

最后——比你优秀的对手在学习,你的仇人在磨刀,你的闺蜜在减肥,隔壁老王在练腰, 我们必须不断学习,否则我们将被学习者超越!

趁年轻,使劲拼,给未来的自己一个交代!