学习ThreadLocal的基本使用以及了解其核心原理实现。jdk版本:1.8
线程程序介绍
早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。
关于其变量
ThreadLocal很容易让人望文生义,想当然地认为是一个“本地线程”。其实,ThreadLocal并不是一个Thread,而是Thread的局部变量,也许把它命名为ThreadLocalVariable更容易让人理解一些。
所以,在Java中编写线程局部变量的代码相对来说要笨拙一些,因此造成线程局部变量没有在Java开发者中得到很好的普及。
– 摘要自百度百科
在ThreadLocal中,提供了三个核心方法,get、set和remove。通过get赋值、通过set获取值、通过remove删除,使用起来还是非常简单的。
public void contextLoads() throws IOException {
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
threadLocal.set(1);
System.out.println(threadLocal.get());
threadLocal.remove();
System.out.println(threadLocal.get());
System.in.read();
}
接下来从源码的角度分析来了解ThreadLocal的核心原理。为什么他是线程的局部变量、怎么做到线程独占的、会存在什么问题。
public void set(T value) {
//通过currentThread获取到当前执行线程
Thread t = Thread.currentThread();
//这里的ThreadLocalMap当成普通的hashMap来理解
ThreadLocalMap map = getMap(t);
if (map != null)//不为null直接set值,为null则初始化
//key就是ThreadLocal对象本身
map.set(this, value);
else
createMap(t, value);
}
//初始化就是直接new了
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
set方法本身非常简单,就是拿到当前线程的的ThreadLocalMap并赋值,因为key存的就是ThreadLocal对象本身,所以set方法不需要传key。接下来在看一下get方法。
public T get() {
//通过currentThread获取到当前执行线程
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
//如果map是null的情况下做了一下初始化,否则从map中获取值,值本身存放在map.Entry中
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
//初始化操作
private T setInitialValue() {
//初始化值就是一个null
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
get方法同样简单,未初始化的情况下线初始化返回null值,已经初始化的情况下从map中获取值。这里的获取方式类似于1.8之前的hashMap,存放的是Entry数组,通过ThreadLocal的hashcode & entry数组的长度来拿到对应的下标并获取值,后面在来分析这段。
public void remove() {
//同样拿到threadLocalMap执行删除方法
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
三个方法分析完,可以得出,ThreadLocal就是拿到当前线程中的map来执行get、set和remove,key是ThreadLocal自身。可以发现贯穿流程的在于Thread的成员变量ThreadLocalMap,因此有必要了解一下关于ThreadLocalMap的实现。
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.
*/
//注意,这里的entry继承了弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
//ThreadLocal key实际为entry的成员
super(k);
value = v;
}
}
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
//数据存储在entry数组中
private Entry[] table;
/**
* The number of entries in the table.
*/
private int size = 0;
/**
* The next size value at which to resize.
*/
private int threshold; // Default to 0
}
ThreadLocalMap并没有实现map的接口,自身是ThreadLocal的内部类,数据存储在ThreadLocalMap的内部类Entry中,自身维护一个Entry数组(类型1.8之前的hashMap)。在ThreadLocalMap中,是存在内存泄露的问题的,可以带着这个问题来阅读ThreadLocalMap的源码。
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
//类似hashMap获取下标的方式,但是这里是hashCode不是通过hashCode()方法获取的
int i = key.threadLocalHashCode & (len-1);
//这里是采用的开放寻址法来解决的hash碰撞的问题。
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
//遍历寻找到对应的entry并赋值
ThreadLocal<?> k = e.get();
if (k == key) {
//key相同(ThreadLocal相同)则直接赋值
e.value = value;
return;
}
if (k == null) {
//如果key为null
//替换旧值
replaceStaleEntry(key, value, i);
return;
}
}
//如果tab[i]为null就直接创建一个entry并赋值了
tab[i] = new Entry(key, value);
int sz = ++size;
//cleanSomeSlots是为了清除为null的key,解决内存泄露的问题。
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
set的代码逻辑参考如上注释,可以看到,这里的数据结构也是采用的散列表的结构,而关于ThreadLocal的hashcode,采用的Fibonacci Hashing,具体可以去了解一下斐波那契哈希的相关概念。采用了开放寻址法来解决hash碰撞的问题,因为这里的entry是弱引用的实现,因此为了优化可能出现的内存泄露问题,在set、replaceStaleEntry、cleanSomeSlots等方法处都会去清理这些stale key。找到对应的entry后,将value赋值给entry.value。
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 //table[i]没有找到或者不匹配的情况往后寻找
return getEntryAfterMiss(key, i, e);
}
get的代码逻辑其实同set方法的寻找类似,看懂了set方法,get方法其实没有什么难度。
/**
* Remove the entry for key.
*/
private void remove(ThreadLocal<?> key) {
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) {
//remove方法调用的时候,如果匹配到key的时候,清除stale的entry。
e.clear();
expungeStaleEntry(i);
return;
}
}
}
remove会主动清理stale的entry,因为entry是弱引用,所以在使用ThreadLocal的时候要主动去调用remove,这样才能将对应的value移除,被GC回收。虽然在使用get、set方法时候也会cleanSomeSlots,但是需要触发场景。
分析完ThreadLocal和ThreadLocalMap的代码后,可以发现如果在使用ThreadLocal的时候,不主动调用remove方法时,可能会出现ThreadLocalMap中entry的value无法被GC回收的问题。虽然ThreadLocalMap是thread的成员变量,会随着thread的销毁而被回收,但是在日常开发中,我们往往会用到线程池,对于核心线程并不会被回收而是重复使用,导致thread一直存活且thread的成员变量ThreadLocalMap一直存活。因此在不用ThreadLocal后,一定记得调用remove销毁。