Thread Local

Thread Local

1.什么是 Thread Local ?

ThreadLocal 是 Java 中的一个类,用于在多线程环境下实现线程局部变量。简单来说,它提供了一种机制,可以使得每个线程都可以拥有自己独立的变量副本,而不必担心线程间的数据共享问题。

2.为什么需要 Thread Local?

  1. 在多线程环境下, 想要获取一个全局变量在不同线程中复用
  2. 在多线程环境下,防止自己的变量被其它线程篡改

3.使用 Tread Local

3.1 测试Tread Local

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
public class ThreadLocalExample {
// 定义一个 ThreadLocal 变量,存储用户名信息
private static ThreadLocal<String> username = new ThreadLocal<>();

public static void main(String[] args) {
// 在主线程中设置用户名为 "kangkang"
username.set("kangkang");

// 创建并启动两个线程
Thread thread1 = new Thread(() -> {
// 在线程1中获取并打印用户名
System.out.println("Thread1 username: " + username.get());
});

Thread thread2 = new Thread(() -> {
// 在线程2中设置用户名为 "xiaoming"
username.set("xiaoming");
// 获取并打印用户名
System.out.println("Thread2 username: " + username.get());
});

thread1.start();
thread2.start();

// 主线程获取并打印用户名
System.out.println("Main thread username: " + username.get());

// 清除 ThreadLocal 变量
username.remove();
}
}

3.2 结果

从这个结果我们可以发现,它是一个数据结构,有点像HashMap,可以保存”key : value“键值对,但是一个Thread Local只能保存一个,并且各个线程的数据互不干扰。

1
2
3
Thread2 username: xiaoming
Thread1 username: null
Main thread username: kangkang

4.Thread Local 源码分析

4.1 Set()方法执行流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 public void set(T value) {
/**currentThread() 用于获取当前正在执行的线程对象实例 该方法被@IntrinsicCandidate注解
表示 它的实现是由底层的本地代码(通常是 C 或 C++)实现的,这样可以更高效地获取当前线程的引用。
*/
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
  1. 首先,ThreadLocalset 方法会获取当前线程,使用 Thread.currentThread() 方,来获取当前线程对象实例。
  2. 然后,通过 getMap(t) 方法获取当前线程类的 ThreadLocalMap。这个方法会根据当前线程来获取相应的 ThreadLocalMap 对象,如果当前线程没有对应的 ThreadLocalMap,则会返回 null
  3. 接下来是关键的一步,在获取到当前线程的 ThreadLocalMap 后,如果这个 map 不为 null,说明当前线程已经有相关的 ThreadLocalMap,则直接调用 map.set(this, value) 方法,将当前 ThreadLocal 对象和对应的值存入 ThreadLocalMap 中。
  4. 如果获取到的 ThreadLocalMapnull,说明当前线程还没有创建对应的 ThreadLocalMap,则调用 createMap(t, value) 方法创建一个新的 ThreadLocalMap 并将当前 ThreadLocal 对象和对应的值存入其中。

4.2 Get()方法执行流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public T get() {
/**currentThread() 用于获取当前正在执行的线程对象实例 该方法被@IntrinsicCandidate注解
表示 它的实现是由底层的本地代码(通常是 C 或 C++)实现的,这样可以更高效地获取当前线程的引用。
*/
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();
}

ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
  1. 首先,通过 Thread.currentThread() 获取当前线程对象实例。
  2. 然后,使用 getMap(t) 方法获取当前线程对象实例对应的 ThreadLocalMap。如果当前线程尚未创建 ThreadLocalMap,则执行setInitialValue() 来返回一个初始值 null
  3. 如果当前线程已经拥有 ThreadLocalMap,则调用 map.getEntry(this) 方法获取当前 ThreadLocal 实例对应的 Entry 对象。这个 Entry 对象包含了 ThreadLocal 实例和对应的值。
  4. 如果获取到的 Entry 对象不为 null,则将其对应的值强制类型转换为 T 类型并返回。这里使用了 @SuppressWarnings("unchecked") 注解来抑制未检查的类型转换警告,因为 ThreadLocalMap 的实现中 value 是用 Object 类型存储的。
  5. 如果在 ThreadLocalMap 中没有找到与当前 ThreadLocal 实例关联的值(即获取的 Entrynull),则调用 setInitialValue() 方法。这个方法用于设置初始值,通常在第一次调用 get 方法时会执行。

4.3 Thread Local Map 数据结构

ThreadLoalMap是一个类似HashMap的数据结构,但是在ThreadLocal中,并没实现Map接口。

ThreadLoalMap中,也是初始化一个大小16的Entry数组,Entry对象用来保存每一个key-value键值对,只不过这里的key永远都是ThreadLocal对象,通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中。

这里需要注意的是,ThreadLoalMap的Entry是继承WeakReference,和HashMap很大的区别是,Entry中没有next字段,所以就不存在链表的情况了。

5. 哈希冲突

我们知道,当发生哈希冲突时,HashMap 会将具有相同哈希值的键值对放置在同一个桶(数组中的一个元素)中,并以链表结构(或树结构,从 JDK 8 开始)存储这些键值对。这样,每个桶可以容纳多个键值对,从而解决了哈希冲突的问题。

但是ThreadLocalMap只有数组结构 它是如何避免哈希冲突的呢?

5.1 Thread Local Map Set() 方法执行流程

先看看ThreadLoalMap中插入一个key-value的实现

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
private void set(ThreadLocal<?> key, Object value) {

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.refersTo(key)) {
e.value = value;
return;
}

if (e.refersTo(null)) {
replaceStaleEntry(key, value, i);
return;
}
}

tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

  1. tabtable数组的引用,tableThreadLocalMap中用于存储ThreadLocal变量的数组。
  2. lentable数组的长度。
  3. i是计算出的存储位置索引,通过对ThreadLocal的哈希值取模来确定存储位置,每个ThreadLocal对象都有一个hash值threadLocalHashCode,每初始化一个ThreadLocal对象,hash值就增加一个固定的大小0x61c88647
  4. 通过循环遍历tab[i]开始的链表,如果发现已经存在相同的ThreadLocal对象,则更新对应的值为新值。
  5. 如果当前位置的ThreadLocal对象为null,说明此位置的Entry已经失效,需要进行替换操作。
  6. 如果遍历结束还没有找到对应的ThreadLocal对象,说明当前位置为空,需要新建一个Entry对象存储该键值对。
  7. 在设置完值之后,如果需要,会进行清理操作和扩容操作。

6.内存泄露

1
2
3
4
5
6
7
8
9
10
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

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

  • Entry 类是一个静态内部类。
  • 它继承自 WeakReference<ThreadLocal<?>>,这意味着 Entry 对象持有对 ThreadLocal 对象的弱引用。弱引用的特点是,当 ThreadLocal 对象没有强引用时,即没有其他对象持有它时,该 ThreadLocal 对象可以被垃圾回收。
  • Entry 类包含了一个 value 字段,用于存储与 ThreadLocal 对象相关联的值。
  • 构造函数 Entry(ThreadLocal<?> k, Object v) 接收两个参数:k 表示 ThreadLocal 对象,v 表示与之相关联的值。在构造 Entry 对象时,会调用 super(k) 来调用父类 WeakReference 的构造方法,将 ThreadLocal 对象作为参数传递进去,以创建对其的弱引用。同时,将 v 赋值给 value 字段。

x.x 结论

x.1 Thread Local 本身不存值

每个 ThreadLocal 实例都持有一个线程局部变量,这个变量的值是线程相关的,并且每个线程都有自己独立的这个值。因此,我们可以说 ThreadLocal 起到了存储值的作用,而线程实例起到了标识这个值的键的作用。

ThreadLocalMap中的entry键值对存储的就是 Key ThreadLocal实例 和 其关联的值

x.2 Thread Local Map是线程自己的局部变量

ThreadLocalMap是每个线程自己的数据结构,用于存储当前线程与ThreadLocal实例关联的值。每个线程都有自己独立的ThreadLocalMap,这样就保证了线程之间的数据隔离性。

x.3 不适合处理大量数据

在高度冲突的情况下,setget操作的效率可能会降低,因为需要不断地寻找下一个空位置或者匹配的Entry对象。这也提醒我们在使用ThreadLocal时要注意避免过多的冲突,可以通过合理设计ThreadLocal对象的哈希值来减少冲突的概率,或者考虑其他数据结构来代替ThreadLocal,以提高效率。

通过这种设计,我们可以在多线程环境下方便地将线程相关的值与线程关联起来,并且每个线程都可以独立地管理自己的线程本地变量,而不会受到其他线程的影响。


Thread Local
http://example.com/2024/03/14/ThreadLocal/
作者
kangkang
发布于
2024年3月14日
许可协议