ThreadLocal

ThreadLocal

作用

  1. 多个线程之间的数据隔离
  2. 单个线程内部的简化参数(传递上下文)

使用

使用可太简单了,一个set 一个get

1
2
3
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("张三");
String name = threadLocal.get();

原理说明

set和get究竟是怎么回事

set方法

1
2
3
4
5
6
7
8
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

get方法

1
2
3
4
5
6
7
8
9
10
11
12
13
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();
}

由前两行可以看到根据每个currentThread获取了map,因此不同的线程获取的map自然就不同,每个线程只能操控自己的map,而对于同一个线程,不管在哪里获取的map都是同一个。

而对于每个map,key为ThreadLocal这个类,value为我们set的值

当然这只是分析,我们可以进一步看getMap源码验证上图

先看getMap方法体:

1
2
3
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

直接返回了Thread类的一个属性成员

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Thread implements Runnable {

/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

...

到这里就可以完全说明不同的线程获取的map自然就不同,每个线程有自己的map


map真的是map吗

再看看这个map,他叫ThreadLocalMap,跟hashmap一样吗?

部分源码如下:

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
static class ThreadLocalMap {

/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

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

/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;

...

对比hashmap分析,我贴上Hashmap的部分源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {

static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;

...
}

transient Node<K,V>[] table;

...

我们都知道hashmap在树化之前是数组+链表,首先是Entry[] table,类似于hashmap“数组+链表”中的数组Node<K,V>[] table

那链表呢?答案是没有。从上面可以看出,hashmap的有指针域Node<K,V> next;,但是ThreadLocalMap的Entry是一个弱引用

综上所述,ThreadLocalMap其实并没有实现Map接口,只是一个Entry数组


ThreadLocalMap如何解决hash冲突

既然我们知道了ThreadLocalMap只是一个Entry数组,如果定义了多个ThreadLocal

1
2
ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();

存东西的时候遇到hash冲突怎么解决呢,先说结论:hashmap遇到hash冲突后会跟在此下标对应的链表后面,而ThreadLocalMap没有链表,遇到hash冲突后计算下一个下标,直到数组在某个下标处没有值

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

Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1); //1.这样计算hash值

for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) { //4.entry不为空且key不相等,说明hash冲突,一直找下一个位置
ThreadLocal<?> k = e.get();

if (k == key) { //2.key相等,覆盖旧值
e.value = value;
return;
}

if (k == null) { //3.当前位置是空的,就初始化⼀个Entry对象放在位置i上;
replaceStaleEntry(key, value, i);
return;
}
}

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

//nextIndex也贴出来,真的是下一个位置!!
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}

get时候也是一样:计算hash下标–>判断key是否相等–>相等直接返回,不相等看下一个位置–>继续判断是否相等…


内存泄漏问题

key使用了弱引用,不存在内存泄漏问题,但是value存在

先看结构

当我们ThreadLocal tl = new ThreadLocal();的时候,实际上有两个引用指向了ThreadLocal,一个是tl本身是一个强引用,一个是getMap(当前线程)得到的ThreadLocalMap,它里面的Entry的key是一个弱引用,指向了ThreadLocal。

为什么这里要使用弱引用呢?

如果是强引用,则tl和key都是强引用,我们一般都是操作tl,也就是tl = null,但是key依然存在强引用,于是导致无法被gc回收,存在内存泄漏问题。

使用弱引用就不会内存泄漏?

不是的,即使key是弱引用,但是如果key被回收变为null,value无法被访问,依然存在内存泄漏问题。解决的办法是主动调用tl.remove();

对于Entry,key是弱引用,在gc时会被回收,但是value得等到该ThreadLocal/线程没有强引用时才会收,如果线程一直存在,比如线程池会复用线程,那么此时value就一直无法回收,造成内存泄露问题。

解决方法也很简单,养成好习惯,主动调用remove()方法

1
2
3
4
5
6
7
ThreadLocal<String> localName = new ThreadLocal();
try {
localName.set("张三");
……
} finally {
localName.remove();
}

线程之间传递

Java中的对象都是在堆中的,是线程共享的,ThreadLocal也一样,只是通过一些手段来实现线程之间的隔离,如果要实现父子线程之间的数据共享,可以使用InheritableThreadLocal

其原理是判断父组件是否有InheritableThreadLocal,有就直接赋值给子线程

1
2
3
4
//Thread的init方法中
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

常见场景

Spring的事务控制

最著名的肯定是Spring的事务控制了,操作数据库最终都是dao层的JDBC,那么就需要Connection连接对象,原则上来说增删改查操作都由Connection操作。本来也没什么,但是Spring为了把事务都控制在service业务层(虽然这些什么什么层本质一样,是自己抽象出来的概念),做了两件事:

  1. 为了保证原子性:保证单个线程中的数据库操作使用的是同⼀个数据库连接,而且为了让开发者不需要将同一个Connection对象作为参数在dao的函数里传来传去,使用时感知不到Connection对象,于是使用了ThreadLocal+AOP的方式,保证了
  2. 为了保证隔离性:使用ThreadLocal,各个线程之间的事务互不影响

贴一个自己使用的场景:存放登录信息


参考

三太子敖丙ThreadLocal

bilibili马士兵弱引用

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×