来挖一挖ThreadLocal的神秘之处
1. 什么是ThreadLocal
如果我们有两位黄金矿工,而他们只有一个矿袋子时,则两个矿工同时捞上来一个矿石时需要去竞争袋子这个互斥资源,没拿到袋子的只能先等着。为了避免这种情况发生,我们可以给他们一人准备一个矿袋子——废话
在Java并发编程中也是同样的道理,多个线程需要对一个共享变量进行写入时,需要进行同步。那么我们也可以给每个线程准备这个变量的副本,这个副本是线程独享的,这样就可以避免线程安全问题。
ThreadLocal
是JDK包提供的用于支持线程本地变量的一种方式。如果创建了一个ThreadLocal
变量时,那么每个访问这个变量的线程都会有这个变量的一个本地副本。
ThreadLocal使用示例
public class ThreadLocalTest {
static void print(String str) {
System.out.println(str + ":" + threadLocal.get());
threadLocal.remove();
}
//定义一个ThreadLocal变量
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set("线程1的本地副本");
print("线程1");
System.out.println("线程1 remove了自己的副本后,获取变量:" + threadLocal.get());
}
});
Thread threadTwo = new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set("线程2的本地副本");
print("线程2");
System.out.println("线程2 remove了自己的副本后,获取变量:" + threadLocal.get());
}
});
threadOne.start();
threadTwo.start();
}
}
线程1:线程1的本地副本
线程2:线程2的本地副本
线程1 remove了自己的副本后,获取变量:null
线程2 remove了自己的副本后,获取变量:null
可以看到我们只定义了一个ThreadLocal
变量,并且两个线程都对这个变量进行了set
值,但是从之后调用的get
方法可以看出,它们都是互不影响的。
2. ThreadLocal的实现原理
2.1 和Thread之间的关系
先看ThreadLocal的类图
再看到在Thread类中,定义了这两个成员变量:
class Thread implements Runnable {
//...
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
//...
}
也就是说,每个Thread
对象中都包含着属于自己的ThreadLocal
,并且ThreadLocal
是通过ThreadLocalMap
存储的,而ThreadLocalMap
是由一个一个Entry
构成的。
这样的话,就可以知道每个线程的副本都来自于自己的threadLocals
变量中了
不过这只是初步的分析和猜想,接下来就前往源码看一下是否真的是这样吧~
2.2 ThreadLocal的方法
首先先看比较重要的几个方法和他的构造方法
public class ThreadLocal<T> {
//只有一个无参构造方法
public ThreadLocal() {
}
public T get() {
Thread t = Thread.currentThread(); //拿到当前线程
ThreadLocalMap map = getMap(t); //以当前线程为参数得到一个ThreadLocalMap对象
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); //调用ThreadLocalMap的getEntry方法
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
public void set(T value) {
Thread t = Thread.currentThread(); //和上面差不多的逻辑
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value); //调用的是ThreadLocalMap的set方法
else
createMap(t, value); // 如果拿到的map为空,则执行createMap方法
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread()); //同样也是对map进行操作
if (m != null)
m.remove(this);
}
//..省略其他方法
}
可以看到ThreadLocal的操作都是基于ThreadLocalMap
来进行的,接下来看看这个ThreadLocalMap
是啥
2.3 ThreadLocalMap(核心)
它虽然是定义在ThreadLocal
中的一个内部类,但是却占据了ThreadLocal
类2/3的内容
成员变量
先看一下它的成员变量:
static class ThreadLocalMap {
//Entry内部类
static class Entry extends WeakReference<ThreadLocal<?>> {
//成员变量 value
Object value;
//构造方法
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
private static final int INITIAL_CAPACITY = 16;
}
其中还有个内部类Entry
,它是由一个弱引用的ThreadLocal对象作为k(这里埋个内存泄漏的伏笔,后面会具体说明),Object作为v的key-value类型
看上去是不是相当眼熟,16的初始容量,Entry
数组,这不就是和HashMap
差不多嘛。但是每个Entry
并没有next指针,也就是并不是和HashMap
一样的数组+链表的结构,具体结构我们继续往下看
getMap方法
从2.2部分可知,我们调用ThreadLocal的set
方法时,首先调用的是ThreadLocalMap
的getMap
方法
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
//也就是直接将线程的threadLocals这个成员变量直接返回(类型为ThreadLocalMap)
}
在2.2中如果getMap
拿到的ThreadLocalMap
对象为空,则执行createMap
方法,不为空则执行set
方法,我们先看createMap
方法
createMap方法与构造方法
void createMap(Thread t, T firstValue) {
//调用的构造方法new了一个ThreadLocalMap对象
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//按照初始容量16 new了一个Entry数组
table = new Entry[INITIAL_CAPACITY];
//将当前线程的ThreadLocal作为key,得到hashcode,并且与数组长度相与得到索引下标
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//数组对应索引下标插入新元素
table[i] = new Entry(firstKey, firstValue);
//设置元素个数和扩容门限信息
size = 1;
setThreshold(INITIAL_CAPACITY);
}
也就是,当线程第一次对ThreadLocal
类型的公共变量执行set
方法的时候,会new
一个ThreadLocalMap
对象作为自己的成员变量,并对这个Map插入一个键值对,其中key是公共变量ThreadLocal,value由set
方法传入
Set方法
private void set(ThreadLocal<?> key, Object value) {
//拿到之前创建的table,并获取长度
Entry[] tab = table;
int len = tab.length;
//计算对应ThreadLocal的hashcode,并与数组长度-1相与得到索引下表
int i = key.threadLocalHashCode & (len-1);
//开放定址法——线性探测法处理冲突
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//数组对应位置k不为空,并且和传入的key相同,则直接更新value
if (k == key) {
e.value = value;
return;
}
//将key与value都为null的Entry设置为null
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//如果不冲突则直接插入
tab[i] = new Entry(key, value);
//判断是否要扩容
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
//很简单的代码,也就是如果对于索引i,如果i+1超过数组长度了则返回0,否则就返回i+1
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
不同于HashMap
的链地址法处理冲突,ThreadLocalMap
采用的是线性探测法来处理冲突。也就是当计算hash得到地址之后,如果当前地址已经存在元素且key不冲突,则探测下一个地址存不存在元素,不存在则插入,存在则判断是否冲突,若冲突则继续向下一个地址探测……这种方法称为线性探测法
ThreadLocal
往往存放的数据量不会特别大(而且key是弱引用又会被垃圾回收,及时让数据量更小),这个时候开放地址法简单的结构会显得更省空间
getEntry以及相关方法
由2.2部分可知当我们对ThreadLocal调用get
方法时,会调用到这个方法
实际上也是同样的,先按照索引寻址,再根据线性探测法直到找到目标元素为止
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key) //找到了对应的元素时返回
return e;
else
return getEntryAfterMiss(key, i, e); //没找到则进行下面的方法
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
//仍然是线性探测法的思路,往后一个一个找
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
//找不到则返回null
return null;
}
remove方法
首先看ThreadLocal的remove方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
先拿到ThreadLocalMap
对象,如果对象存在则执行ThreadLocalMap
对象的remove
方法
接下来看ThreadLocalMap的remove方法
private void remove(ThreadLocal<?> key) {
//拿到Entry数组和长度
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.get() == key) {
//找到了对应的ThreadLocal对象则调用clear方法将对应Entry设置为null
e.clear();
expungeStaleEntry(i);
return;
}
}
}
3. 总结
如下图所示,在每个线程内部都有一个名为threadLocals
的成员变量,该变量的类型为ThreadLocalMap
。ThreadLocalMap
由Entry
数组组成,其中每个Entry
的key为我们定义的ThreadLocal
的变量的this
引用,value则为我们使用set
方法设置的值。
其中ThreadLocalMap
同样采取的是hash索引的方式存储,并且使用的是开放定址法中的线性探测法来处理hash冲突问题。
每当我们使用某个线程第一次对ThreadLocal变量使用get或者set方法时,会在当前线程内创建一个新的ThreadLocalMap
对象,并且将key作为ThreadLocal,value使用get方法时会设置为null(这部分源码细节没放出来),使用set方法时会设置为我们指定的值。并将这个key-value对象作为Entry存入数组中。
4. 补充——关于内存泄漏与内存溢出
防止内存泄漏
之前提到过,在Entry中对于key是弱引用的,而对于value是强引用的
static class Entry extends WeakReference<ThreadLocal<?>> {
//...
}
其中关于弱引用,《深入理解JVM虚拟机》中是这样描述的:
弱引用是用来描述那些非必须对象,但是它的强度比软引用更弱一些,
被弱引用关联的对象只能够生存到下一次垃圾回收发生为止。
当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
至于为什么会设置成弱引用呢?假设我们使用的是强引用,那么由于每个线程访问某 ThreadLocal 变量后,都会在自己的 map 内维护该 ThreadLocal 变量与具体实例的映射,如果我们不删除这个引用,则这些ThreadLocal不能被回收,这样就会导致内存泄漏问题。
而我们使用弱引用的话,如果没有强引用来指向ThreadLocal变量时,它就可被回收,从而避免上面所说的问题。
但这样又会出现另外一个问题,当ThreadLocal变量被回收之后,Entry的键就会变为null了,并且这个Entry无法被移除,从而导致另一种内存泄漏。针对这个问题,ThreadLocal采用下面两个方法来解决:
replaceStaleEntry
:将所有key为null的Entry的value设置为null,使得该value可以被回收expungeStaleEntry
:将key和value为null的Entry设置为null,使得该Entry可以被回收
其中set
方法中在for循环遍历中使用到了上面的方法,而remove
和getMap
使用到了下面的方法
内存溢出
通过上面的分析,我们知道expungeStaleEntry
方法是帮助垃圾回收的,根据源码,我们可以发现get
和set
方法都可能触发清理方法expungeStaleEntry
,所以正常情况下是不会有内存溢出的。
但是如果我们没有调用get
和set
的时候就会可能面临着内存溢出,特别是线程池的情况下,当我们使用线程池来存储对象时,因为线程池有很长的生命周期,所以线程池会一直持有value值,那么垃圾回收器就无法回收value,所以就会导致内存一直被占用,从而导致内存溢出问题的发生。
ThreadLocal
内存溢出的解决方案很简单,我们只需要在使用完ThreadLocal
之后,执行remove
方法就可以避免内存溢出问题的发生了。经过2.3部分的分析我们可知,调用remove
方法会移除整个Entry,所以即使线程一直存活着也不会因为ThreadLocalMap
的内存占用导致内存溢出了