学习-学个屁

ThreadLocal是什么

2021-09-23

本文总阅读量

ThreadLocal是什么?怎么用?为什么要用?他有什么缺点?如何避免

ThreadLocal是什么

在了解ThreadLocal之前,我们先了解下什么是线程封闭

把对象封闭在一个线程里,即使这个对象不是线程安全的,也不会出现并发安全问题

实现线程封闭大致有三种方式:

  1. 栈封闭:就是用栈(stack)来保证线程安全
1
2
3
4
public void testThread() {
StringBuilder sb = new StringBuilder();
sb.append("Hello");
}

StringBuilder不是线程安全的,但是他只是个局部变量局部变量是存储在虚拟机栈的,虚拟机栈是线程隔离的,所以不会有线程安全问题。

  1. Ad-hoc线程封闭:维护线程封闭性的职责完全有线程来承担,不推荐使用。
  2. ThreadLocal线程封闭,简单易用

第三种方式就是使用ThreadLocal来实现线程封闭,线程封闭的指导思想是封闭,而不是共享。所以说ThreadLocal是用来解决变量共享的并发安全问题,是有些不精确的。

其实,从JDK1.2开始,提供的ThreadLocal使用就非常简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ThreadLocalDemo {

public static void main(String[] args) throws InterruptedException {

final ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("main-thread : Hello");

Thread thread = new Thread(() -> {
// 获取不到主线程设置的值,所以为null
System.out.println(threadLocal.get());
threadLocal.set("sub-thread : World");
System.out.println(threadLocal.get());
});
// 启动子线程
thread.start();
// 让子线程先执行完成,再继续执行主线
thread.join();
// 获取到的是主线程设置的值,而不是子线程设置的
System.out.println(threadLocal.get());
threadLocal.remove();
System.out.println(threadLocal.get());
}
}

运行结果:

1
2
3
4
null
sub-thread : World
main-thread : Hello
null

运行结果说明了ThreadLocal只能获取本线程设置的值,这也体现了线程封闭的问思想。基本上,ThreadLocal对外提供的方法只有三个,get(),set(T),remove()。

原理

使用方式非常简单,所以我们看下ThreadLocal的源码,ThreadLocal内部定义了一个静态ThreadLocalMap类,ThreadLocalMap内部又定义了一个Entry类,这里只看一些比较重要的属性和方法。

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
public class ThreadLocal<T> {

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 set(T value) {
//获取当前线程对象
Thread t = Thread.currentThread();
//根据当前线程对象获取ThreadLocalMap(ThreadLocalMap被Thread(线程)所持有)
ThreadLocalMap map = getMap(t);
//如果ThreadLocalMap存在,则直接插入,不存在则新建ThreadLocalMap
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

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

// 从这里可以看出ThreadLocalMap对象是被Thread类持有的
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

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

// 内部类ThreadLocalMap
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
// 内部类Entity,实际存储数据的地方
// Entry的key是ThreadLocal对象,不是当前线程ID或者名称
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// 注意这里维护的是Entry数组
private Entry[] table;
}
}

根据上面的源码,大致画出ThreadLocal在虚拟机中的内存结构:
ThreadLocal内存结构

实箭头表示强引用,虚箭头表示弱引用,需要注意的是:

  • ThreadLocalmap虽然是在ThreadLocal中定义的,但是实际上被Thread(当前线程)所持有
  • Entry的key是ThreadLocal对象,而不是当前线程的ID或者线程名称。
  • ThreadLocalMap持有的是Entry数组,而不是Entry实例

对于第一点,ThreadLocalMap对象被hread(线程)所持有,是为了实现每个线程都有自己独立的ThreadLocalMap对象,以此为基础,做到线程的隔离,第二点和第三点,我们先来想一个问题,如果一个线程中定义了多个ThreadLocal对象,内存结构应该如何,在此之前,我们先看set方法的源码:

1
2
3
4
5
6
7
8
9
10
11
public void set(T value) {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 根据线程对象获取ThreadLocalMap对象(ThreadLocalMap被Thread持有)
ThreadLocalMap map = getMap(t);
// 如果ThreadLocalMap存在,则直接插入;不存在,则新建ThreadLocalMap
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

也就是说,如果程序定义了多个ThreadLocal对象的话,并不会创建多个ThreadLocalMap对象,而会共用一个ThreadLocalMap对象,所以内存结构应该是这样:
内存结构

这个内存结构解释了第二点和第三点,如果Entry中的key是当前线程的ID或者名称的话,那么程序中定义多个ThreadLocal对象的时,Entry数组中的所有的Entry的key都一样(因为他们处于同一个线程中),或者说他们只能存一个value,ThreadLocalMap中持有的时Entry数组,而不是Entry,则是因为程序可以定义多个ThreadLocal对象,自然而然需要一个数组来存储。

内存泄漏

ThreadLocal会发生内存泄漏吗?

因为Entry数组通过ThreadLocalMap最终被Thread持有,并且是强引用,也就是说Entry数组对象的生命周期和当前线程是一样的,即使ThreadLocal对象被回收了,Entry数组也不一定会被回收,因为线程可能没被回收,这样也就有可能会发生内存泄漏,ThreadLocal在设计的时候就提供了一些补救措施:

  • Entry数组的key是弱引用的ThreadLocal对象,很容易被回收,导致key为null(但是value不为null),所以在调用get,set,remove的时候会自动清理key为null的Entry
  • 同时remove方法也是用来清理无用对象,防止内存泄漏的,所以每次用完ThreadLocal对象后需要手动调用remove方法。
    有些文章认为是弱引用导致了内存泄漏,其实是不对的。假设把所有的弱引用都改成强引用,这样无用对象的key和value都不会为null,反而更不利于回收。只能通过调用remove方法手动清理,或者等待线程结束生命周期。也就是说ThreadLocalMap的生命周期由他的线程来决定,线程如果不中断,ThreadLocalMap就不会被GC回收,这才是ThreadLocal内存泄漏的真正原因。

应用场景

  • 维护JDBC的java.sql.Connection对象,因为每个线程都需要保持特定的Connection对象。
  • Web开发时,有些信息需要从controller传到service传到dao,甚至传到util类。看起来非常不优雅,这时便可以使用ThreadLocal来优雅的实现。
  • 包括线程不安全的工具类,比如Random、SimpleDateFormat等

与synchronized的关系

  • synchronized是同一时间最多只有一个线程执行,所以变量只需要存一份,是一种时间换空间的方式
  • ThreadLocal是多个线程之间互不影响,所以每个线程存一份变量,是一种空间换时间的方式
使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

扫描二维码,分享此文章