我们创建的变量是可以被任何⼀个线程访问并修改的。而ThreadLocal 类主要解决的就是让每个线程绑定⾃⼰的值,可以将 ThreadLocal 类形象的⽐喻成存放数据的盒⼦,盒⼦中可以存储每个线程的私有数据。

ThreadLocal类用来提供线程内部的局部变量,这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量(这个变量不能是共享变量本身)。

概念引入

ThreadLocal的常用方法

方法声明 描述
ThreadLocal() 创建ThreadLocal对象
initialValue() 1. 返回当前线程对应的”初始值”,只在调用get的时候才触发(延迟加载
2. 如果调用get方法前调用了set方法,不会调用此方法
3. 一般情况每个线程最多调用一次本方法,除非他调用了remove()
4. 不重写此方法,默认返回null
public void set( T value) 设置当前线程绑定的局部变量
public T get() 获取当前线程绑定的局部变量:
1. 取出当前线程的ThreadLocalMap,然后调用map.getEnty,获取本ThreadLocal的value
2. ThreadLocalMap以及其key/value都是保存在Thread中,而不是ThreadLocal
public void remove() 移除当前线程绑定的局部变量

ThreadLocal结构

image-20211026213644475

注意:ThreadLocalMap中的key为弱引用,这里先不纠结,后面会介绍

Java的四种引用类型

  • 强引用:我们常常new出来的对象就是强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候
  • 软引用:使用SoftReference修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收
  • 弱引用:使用WeakReference修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收
  • 虚引用:虚引用是最弱的引用,在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知

简单案例

普通多线程

首先我们先看一段简单的多线程示例

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
public class MyDemo {
private String content;

private String getContent() {
return content;
}

private void setContent(String content) {
this.content = content;
}

public static void main(String[] args) {
MyDemo demo = new MyDemo();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
demo.setContent(Thread.currentThread().getName() + "的数据");
System.out.println("-----------------------");
System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
}
});
thread.setName("线程" + i);
thread.start();
}
}
}

// 运行结果(不唯一)
-----------------------
-----------------------
-----------------------
线程2--->线程4的数据
-----------------------
线程1--->线程4的数据
-----------------------
线程3--->线程3的数据
线程0--->线程3的数据
线程4--->线程4的数据

从结果可以看出多个线程在访问同一个变量的时候出现的异常,线程间的数据没有隔离。

解决方案

synchronized解决

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
public class Demo02 {

private String content;

public String getContent() {
return content;
}

public void setContent(String content) {
this.content = content;
}

public static void main(String[] args) {
Demo02 demo02 = new Demo02();
for (int i = 0; i < 5; i++) {
Thread t = new Thread(){
@Override
public void run() {
synchronized (Demo02.class){
demo02.setContent(Thread.currentThread().getName() + "的数据");
System.out.println("-------------------------------------");
String content = demo02.getContent();
System.out.println(Thread.currentThread().getName() + "--->" + content);
}
}
};
t.setName("线程" + i);
t.start();
}
}
}
// 运行结果
-----------------------
线程0--->线程0的数据
-----------------------
线程3--->线程3的数据
-----------------------
线程2--->线程2的数据
-----------------------
线程1--->线程1的数据
-----------------------
线程4--->线程4的数据

从结果可以发现, 加锁确实可以解决这个问题,但是在这里我们强调的是线程数据隔离的问题,并不是多线程共享数据的问题, 而且加锁也会导致程序的效率变低,所以在这个案例中使用synchronized关键字是不合适的。

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
public class MyDemo {
static ThreadLocal<String> threadLocal = new ThreadLocal<>();

private String content;

private String getContent() {
return threadLocal.get();
}

private void setContent(String content) {
threadLocal.set(content);
}

public static void main(String[] args) {
MyDemo demo = new MyDemo();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
demo.setContent(Thread.currentThread().getName() + "的数据");
System.out.println("-----------------------");
System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
}
});
thread.setName("线程" + i);
thread.start();
}
}
}
// 运行结果
-----------------------
线程0--->线程0的数据
-----------------------
线程1--->线程1的数据
-----------------------
线程2--->线程2的数据
-----------------------
线程3--->线程3的数据
-----------------------
线程4--->线程4的数据

从结果来看,这样很好的解决了多线程之间数据隔离的问题,十分方便。

ThreadLocal的优点

  • 每一个Thread内部都有自己的副本,线程之间不共享
  • 无需加锁,提高执行效率
  • 更高效利用内存、节省开销
  • 免去传参烦恼:在任何方法中都能很方便的获取到该对象,不同多级传递参数,降低了代码耦合度。

ThreadLocal&synchronized对比

虽然ThreadLocal模式与synchronized关键字都用于处理多线程并发访问变量的问题, 不过两者处理问题的角度和思路不同。

synchronized ThreadLocal
原理 同步机制采用’以时间换空间’的方式, 只提供了一份变量,让不同的线程排队访问 ThreadLocal采用’以空间换时间’的方式, 为每一个线程都提供了一份变量的副本,从而实现同时访问而相不干扰
侧重点 多个线程之间访问资源的同步性 多线程中让每个线程之间的数据相互隔离

场景实战

常见用途

ThreadLocal有两种典型的使用场景,可以概括为:

线程隔离

ThreadLocal 中数据只属于当前线程,其本地值对别的线程是不可见的,在多线程环境下,可以防止自己的变量被其他线程篡改。另外,由于各个线程之间的数据相互隔离,避免同步加锁带来的性能损失,大大提升了并发性的性能。

数据库连接独享、SimpleDateFormat、Random等场景经常需要配合使用。

跨函数传递数据

同一个线程内,跨类、跨方法传递数据时,如果不用 ThreadLocal,那么相互之间的数据传递势必要靠返回值和参数,这样无形之中增加了这些类或者方法之间的耦合度。

一般用来传递需要在函数之间频繁传递的数据、HTTP 的用户请求实例 HttpRequest、请求过程中的用户会话(Session)

场景案例

场景一:线程隔离,每个线程都需要一个独享的对象

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
public class ThreadLocalTest {

public static ExecutorService threadPool = Executors.newFixedThreadPool(10);

public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(() -> {
String date = new ThreadLocalTest().date(finalI);
System.out.println(date);
});
}
threadPool.shutdown();
}

public String date(int seconds) {
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
return dateFormat.format(date);
}
}

class ThreadSafeFormatter {
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
}

场景二:跨函数传递数据

有时候我们会遇到多层级调用,然后同一基本信息需要不断被传递的场景,如果我们只是简单的将参数在各个方法上传递就会显得代码耦合度很高,也难以维护。这个时候,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
public class ThreadLocalTest2 {

public static void main(String[] args) {
new UserService().getUser();
}
}

class UserService {
public void getUser() {
// 模拟获取用户基本信息
User user = new User("user1");
UserContextHolder.holder.set(user);
new UserInfoService().getUserInfo();
}
}

class UserInfoService {
public void getUserInfo() {
User user = UserContextHolder.holder.get();
System.out.println("getUserInfo====>" + user.name);
//获取用户信息......
new UserAddress().getAddress();
}
}

class UserAddress {
public void getAddress() {
User user = UserContextHolder.holder.get();
System.out.println("getAddress====>" + user.name);
//获取用户地址信息......
}
}

class UserContextHolder {
public static ThreadLocal<User> holder = new ThreadLocal<>();
}

class User {
String name;
public User(String name) {
this.name = name;
}
}

使用方式推荐

由于 ThreadLocal 使用不当会导致严重的内存泄漏,所以为了更好的避免内存泄漏的发生,我们使用 ThreadLocal 时遵守以下两个原则:

(1)尽量使用 private static final 修饰 ThreadLocal 实例。使用 private 与 final 修饰符,主要是尽可能不让他人修改、变更 ThreadLocal 变量的引用; 使用 static 修饰符主要为了确保 ThreadLocal 实例的全局唯一。

(2)ThreadLocal 使用完成之后务必调用 remove 方法。这是简单、有效地避免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
public class ThreadLocalDemo01 {

/**
* 设置线程池大小
*/
public static final Integer MAX_THREAD_NUM = 5;

/**
* 定义ThreadLocal变量
*/
private final static ThreadLocal<Product> threadLocal = ThreadLocal.withInitial(Product::new);

static class Product {
AtomicInteger num = new AtomicInteger();

public void produce() {
num.incrementAndGet();
}
}

public static void main(String[] args) throws InterruptedException {
// 方便测试用的线程池,生产环境不要这么写
ExecutorService executorService = Executors.newFixedThreadPool(MAX_THREAD_NUM);

for (int i = 0; i < MAX_THREAD_NUM; i++) {
int finalI = i;
executorService.execute(() -> {
for (int j = 0; j < finalI + 1; j++) {
threadLocal.get().produce();
}
System.out.println(Thread.currentThread() + ":" + threadLocal.get().num.get());
// 使用完以后移除,
threadLocal.remove();
});
}
}
}

源码分析

JDK8 ThreadLocal的设计是:每个Thread维护一个ThreadLocalMap哈希表,这个哈希表的keyThreadLocal实例本身,value才是真正要存储的值Object

(1) 每个Thread线程内部都有一个Map (ThreadLocalMap)
​ (2) Map里面存储ThreadLocal对象(key)和线程的变量副本(value)
​ (3)Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
​ (4)对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

在这里插入图片描述

ThreadLocal核心方法源码

基于ThreadLocal的内部结构,我们继续探究一下ThreadLocal的核心方法源码,更深入的了解其操作原理。

除了构造之外, ThreadLocal对外暴露的方法有以下4个:

方法声明 描述
protected T initialValue() 返回当前线程局部变量的初始值
public void set( T value) 设置当前线程绑定的局部变量
public T get() 获取当前线程绑定的局部变量
public void remove() 移除当前线程绑定的局部变量

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
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
   /**
* 返回当前线程中保存ThreadLocal的值
* 如果当前线程没有此ThreadLocal变量,
* 则它会通过调用{@link #initialValue} 方法进行初始化值
*
* @return 返回当前线程对应此ThreadLocal的值
*/
public T get() {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null) {
// 以当前的ThreadLocal 为 key,调用getEntry获取对应的存储实体e
ThreadLocalMap.Entry e = map.getEntry(this);
// 找到对应的存储实体 e
if (e != null) {
@SuppressWarnings("unchecked")
// 获取存储实体 e 对应的 value值
// 即为我们想要的当前线程对应此ThreadLocal的值
T result = (T)e.value;
return result;
}
}
// 如果map不存在,则证明此线程没有维护的ThreadLocalMap对象
// 调用setInitialValue进行初始化
return setInitialValue();
}

/**
* set的变样实现,用于初始化值initialValue,用于代替防止用户重写set()方法
*
* @return the initial value 初始化后的值
*/
private T setInitialValue() {
// 调用initialValue获取初始化的值
T value = initialValue();
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null)
// 存在则调用map.set设置此实体entry
map.set(this, value);
else
// 1)当前线程Thread 不存在ThreadLocalMap对象
// 2)则调用createMap进行ThreadLocalMap对象的初始化
// 3)并将此实体entry作为第一个值存放至ThreadLocalMap中
createMap(t, value);
// 返回设置的值value
return value;
}

/**
* 获取当前线程Thread对应维护的ThreadLocalMap
*
* @param t the current thread 当前线程
* @return the map 对应维护的ThreadLocalMap
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
*创建当前线程Thread对应维护的ThreadLocalMap
*
* @param t 当前线程
* @param firstValue 存放到map中第一个entry的值
*/
void createMap(Thread t, T firstValue) {
//这里的this是调用此方法的threadLocal
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

大致流程如下:

  • 先尝试获得当前线程,然后获得当前线程的 ThreadLocalMap 成员,暂存于 map 变量。

  • 如果获得的 map 不为空,以当前 threadlocal 实例为 Key 尝试获得 map 中的 Entry(条目)。

  • 如果 Entry 条目不为空,返回 Entry 中的 Value。

  • 如果 Entry 为空,则通过调用 initialValue 初始化钩子函数获取“ThreadLocal”初始值, 并设置在 map 中。如果 map 不存在,还会给当前线程创建新 ThreadLocalMap 成员,并绑定第一个“Key-Value 对”。

set方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 设置当前线程对应的ThreadLocal的值
*
* @param value 将要保存在当前线程对应的ThreadLocal的值
*/
public void set(T value) {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null)
// 存在则调用map.set设置此实体entry
map.set(this, value);
else
// 1)当前线程Thread 不存在ThreadLocalMap对象
// 2)则调用createMap进行ThreadLocalMap对象的初始化
// 3)并将此实体entry作为第一个值存放至ThreadLocalMap中
createMap(t, value);
}

大致流程如下:

  • 获得当前线程,然后获得当前线程的 ThreadLocalMap 成员,暂存于 map 变量。

  • 如果 map 不为空,则将 Value 设置到 map 中,当前的 Threadlocal 作为 key。

  • 如果 map 为空,给该线程创建 map,然后设置第一个“Key-Value 对”,Key 为当前的ThreadLocal 实例,Value 为 set 方法的参数 value 值。

remove方法

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 删除当前线程中保存的ThreadLocal对应的实体entry
*/
public void remove() {
// 获取当前线程对象中维护的ThreadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
// 如果此map存在
if (m != null)
// 存在则调用map.remove
// 以当前ThreadLocal为key删除对应的实体entry
m.remove(this);
}

initialValue方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 返回当前线程对应的ThreadLocal的初始值

* 此方法的第一次调用发生在,当线程通过{@link #get}方法访问此线程的ThreadLocal值时
* 除非线程先调用了 {@link #set}方法,在这种情况下,
* {@code initialValue} 才不会被这个线程调用。
* 通常情况下,每个线程最多调用一次这个方法。
*
* <p>这个方法仅仅简单的返回null {@code null};
* 如果程序员想ThreadLocal线程局部变量有一个除null以外的初始值,
* 必须通过子类继承{@code ThreadLocal} 的方式去重写此方法
* 通常, 可以通过匿名内部类的方式实现
*
* @return 当前ThreadLocal的初始值
*/
protected T initialValue() {
return null;
}

ThreadLocalMap源码分析

ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能(Entry [] table),其内部的Entry也是独立实现。

(1)成员变量

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
/**
* 初始容量 —— 必须是2的整次幂
*/
private static final int INITIAL_CAPACITY = 16;

/**
* 存放数据的table,Entry类的定义在下面分析
* 同样,数组长度必须是2的整次幂。
*/
private Entry[] table;

/**
* 数组里面entrys的个数,可以用于判断table当前使用量是否超过负因子。
*/
private int size = 0;

/**
* 进行扩容的阈值,表使用量大于它的时候进行扩容。
*/
private int threshold; // Default to 0

/**
* 阈值设置为长度的2/3
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}

(2) 存储结构 - Entry

1
2
3
4
5
6
7
8
9
10
11
12
// 在ThreadLocalMap中,也是用Entry来保存K-V结构数据的。但是Entry中key只能是ThreadLocal对象,这点被Entry的构造方法已经限定死了
// 另外,Entry继承WeakReference,使用弱引用,可以将ThreadLocal对象的生命周期和线程生命周期解绑,持有对ThreadLocal的弱引用,可以使得ThreadLocal在没有其他强引用的时候被回收掉,这样可以避免因为线程得不到销毁导致ThreadLocal对象无法被回收

static class Entry extends WeakReference<ThreadLocal> {
/** The value associated with this ThreadLocal. */
Object value;

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

常见问题

ThreadLocal 内存泄露问题

不再用到的内存,没有及时释放,就叫做内存泄漏。对于持续运行的服务进程,必须及时释放内存,否则内存占用率越来越高,轻则影响 系统性能,重则导致进程崩溃。

由上文我们知道了,ThreadLocalMap 中使⽤的 key 为 ThreadLocal 的弱引⽤,⽽ value 是强引⽤。所以,如果ThreadLocal 没有被外部强引⽤的情况下,在垃圾回收的时候,key 会被清理掉,⽽ value 不会被清理掉。这样⼀来, ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远⽆法被GC 回收,这个时候就可能会产⽣内存泄露。

ThreadLocalMap实现中已经考虑了这种情况,在调⽤ set() 、remove() 、rehash() ⽅法的时候,会清理掉key为 null 的记录。

但是ThreadLocal不被使用时,这几个方法也不会被调用,这种情况下,线程只要不终止,还是可能出现内存泄露的问题。所以我们使⽤完ThreadLocal ⽅法后,最好⼿动调⽤remove()⽅法,这样就能避免这个问题了。

空指针异常

有时候,我们在调用get方法之前没有set,也没有重写initialValue,这个时候很可能因为自动拆装箱问题导致空指针异常:

1
2
3
4
5
6
7
8
9
10
11
12
public class ThreadLocalTest2 {
public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
// 返回值类型和ThreadLocal一致就不会异常而不一致就会涉及拆装箱问题
// 因为没有set,也没有重写initialValue方法,所以threadLocal.get()的值是null,
// public static Long get(){
public static long get(){
return threadLocal.get();
}
public static void main(String[] args) {
get();
}
}