引子

现在有这样一个需求,在多线程并发的场景下,要求每个线程中的变量都是相互独立的
举个例子:线程 1 为变量 A 设置一个值 1,那么他获取到值的只能是线程 1 设置进去的值 ;线程 2 为变量 A 设置一个值 2,那么他获取到的值的只能是线程 2 设置进去的值

下面是一个 Demo

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 Demo {

/**
* 实例变量
*/
private String A;

public void setA(String a) {
A = a;
}

public String getA() {
return A;
}

public static void main(String[] args) {
Demo demo = new Demo();

//创建5个线程
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(() -> {
//每个线程为变量A赋值,然后取出来该线程为变量A赋的值
demo.setA(Thread.currentThread().getName() + "设置的值");
System.out.println(Thread.currentThread().getName() + "---->取出" + demo.getA());
});
thread.setName("线程" + i);
thread.start();
}
}
}

多测试几次,有时候就会出现的运行结果

线程 2—->取出线程 3 设置的值
线程 4—->取出线程 4 设置的值
线程 3—->取出线程 3 设置的值
线程 1—->取出线程 1 设置的值
线程 0—->取出线程 3 设置的值

ThreadLocal 简介

ThreadLocal 是除了加锁这种同步方式之外的一种规避多线程访问出现线程不安全的方法

ThreadLocal 是 java.lang包下的一个类,它能提供线程局部变量,这些变量与正常的变量不同,因为每一个线程在访问 ThreadLocal 实例的时候(通过 get 或 set 方法)都有自己独立初始化的变量副本,即这种变量能在多线程环境下访问(即通过 get 和 set 方法访问)时能保证各个线程的变量相对独立于其他线程内的变量(变量在线程的生命周期内起作用)。

如果创建一个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,线程之间是相互隔离的,各个线程操作的都是自己本地内存中的变量,从而规避了线程安全问题。

ThreadLocal 实例通常来说都是 private static类型的,用于关联线程和线程上下文。

案列改造

利用 ThreadLocal 的 set 方法和 get 方法来对上述案例进行改造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 修改一,使用ThreadLocal来解决
* 创建ThreadLocal对象
*/
ThreadLocal<String> threadLocal = new ThreadLocal();

/**
* 修改二
* 设置变量值到本地变量中
* @param a
*/
public void setA(String a) {
threadLocal.set(a);
}

/**
* 修改三
* 返回本地线程的值
* @return
*/
public String getA() {
return threadLocal.get();
}

此时再进行多次测试,线程就只能获取到自己设置进去的值了

与 synchronized 区别

通过 synchronized 关键字也可以解决问题,将当前类作为锁对象即可

1
2
3
4
5
6
7
Thread thread = new Thread(() -> {
synchronized (Demo.class) {
//每个线程为变量A赋值,然后取出来该线程为变量A赋的值
demo.setA(Thread.currentThread().getName() + "设置的值");
System.out.println(Thread.currentThread().getName() + "---->取出" + demo.getA());
}
});

ThreadLocal 与 synchronized 的区别

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

synchronizedThreadLcocal
原理时间换空间
只提供了一份变量让不同的线程排队访问
空间换时间
为每一个线程都提供了一份变量的副本从而实现同时访问而相不干扰
侧重点多个线程之间访问资源的同步多线程中让每个线程之间的数据相互隔离

ThreadLocal 设计方案

在 JDK8 中 ThreadLocal 的设计方案是:

每个 Thread 维护一个 ThreadLocalMap,这个 Map 的 key 是 ThreadLocal 实例本身,value 才是真正要存储的值 Object

具体的过程

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

好处

  • ThreadLocal 负责设置值,可以使 Map 中储存的 Entry 数量变少(如果由 Thread 设置值,多少个线程就有多少个 key-value)
  • Thread 使用完毕销毁时,ThreadLocalMap 也会跟着销毁,可以减少内存的开销(如果是 ThreadLocalMap 维护,线程销毁 Map 还在)

核心方法源码分析

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

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

set 方法

设置当前线程对应的 ThreadLocal 的值

1
2
3
4
5
6
7
8
9
10
11
12
public void set(T value) {
//获取当前线程对象(调用者线程)
Thread t = Thread.currentThread();
//以当前线程作为key,获取此线程中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
//如果map不为null,就直接添加本地变量,key为当前定义的ThreadLocal变量的this引用,value为添加的本地变量值
if (map != null)
map.set(this, value);
//如果map为null,说明首次添加,需要首先创建出对应的map
else
createMap(t, value);
}

先看一下 getMap() 方法

1
2
3
4
5
6
7
8
9
/**
* 获取当前线程对应的ThreadLocalMap
*
* @param t the current thread 当前线程
* @return the map 对应维护的ThreadLocalMap
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

看一下返回的 threadLocals,可以发现位于 Thread 类的内部,并且默认值为 null

1
2
/*与此线程有关的 ThreadLocal 值。该map由 ThreadLocal 类维护*/
ThreadLocal.ThreadLocalMap threadLocals = null;

再看一下createMap()方法

1
2
3
4
void createMap(Thread t, T firstValue) {
//以当前变量this作为key,要添加的本地变量值作为value
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

get 方法

先获取当前线程的 ThreadLocalMap 变量,如果存在则返回值,不存在则创建并返回初始值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//以当前线程作为key,获取此线程中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
//如果map不为null,就可以在map中查找到本地变量的值
if (map != null) {
//Entry: 键值对对象,获取Entry
ThreadLocalMap.Entry e = map.getEntry(this);
//如果Entry不为空
if (e != null) {
@SuppressWarnings("unchecked")
//获取Entry对应的value
T result = (T)e.value;
return result;
}
}
//如果map不存在,或者获取的entry为空,都会调用这个方法
//执行到此处,threadLocals为null,调用该更改初始化当前线程的threadLocals变量
return setInitialValue();
}

进入 setInitialValue():初始化值并返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private T setInitialValue() {
//protected T initialValue() {return null;}
//调用initialValue()获取初始化的值
//此方法可以被子类重写,如果不重写,默认返回的就是null
T value = initialValue();
//获取当前线程对象
Thread t = Thread.currentThread();
//以当前线程作为key,获取此线程中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
//如果map不为null,就直接添加本地变量,key为当前线程,value为添加的本地变量值
if (map != null)
map.set(this, value);
else
//如果map为null,说明首次添加,需要首先创建出对应的map
createMap(t, value);
return value;
}

remove 方法

获取当前线程,并根据当前线程获取一个 Map,如果获取的 Map 不为空,则移除当前 ThreadLocal 对象对应的 entry

1
2
3
4
5
6
7
8
9
10
/**
* 删除当前线程中保存的ThreadLocal对应的实体entry
*/
public void remove() {
//获取当前线程中维护的ThreadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
//如果map不为null,就移除当前线程中指定ThreadLocal实例的本地变量
if (m != null)
m.remove(this);
}

initialValue 方法

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

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

内存泄漏

每个线程内部有一个名为 threadLocals 的成员变量,该变量的类型为 ThreadLocal.ThreadLocalMap 类型(在上面 set 方法里面提到过,位于 Thread 类的内部,可以理解为类似于一个 HashMap),其中的 key 为当前定义的 ThreadLocal 变量的 this 引用,value 为我们使用 set 方法设置的值。每个线程的本地变量存放在自己的本地内存变量 threadLocals 中

如果当前线程一直不消亡,那么这些本地变量就会一直存在(所以可能会导致内存溢出),因此使用完毕需要将其 remove 掉。

比如说创建的线程池有核心线程是一直存在的,那么它维护的 ThreadLocalMap 也是一直存在的,所以可能会导致内存溢出。