ThreadLocal
的字面意思就是线程的本地变量,也就是说线程的局部变量。ThreadLocal
在每个线程中都有自己的副本,亦即一个线程一份数据,相互之间不影响。 ThreadLocal提供get
和set
访问方法,使用get时总是返回set方法的最新值。ThreadLocal
一个典型的应用就是减少一个线程内的参数传递,如数据库连接JDBCConnection或用户的Session,避免每个方法调用都要传递这个参数。 Spring的事务处理就大量使用了ThreadLocal
,如TransactionSynchronizationManager这个类。
java
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal("Transactional resources");
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations = new NamedThreadLocal("Transaction synchronizations");
private static final ThreadLocal<String> currentTransactionName = new NamedThreadLocal("Current transaction name");
private static final ThreadLocal<Boolean> currentTransactionReadOnly = new NamedThreadLocal("Current transaction read-only status");
private static final ThreadLocal<Integer> currentTransactionIsolationLevel = new NamedThreadLocal("Current transaction isolation level");
private static final ThreadLocal<Boolean> actualTransactionActive = new NamedThreadLocal("Actual transaction active");
ThreadLocal用法示例
java
package org.encyclopedia.thread;
import java.util.concurrent.CountDownLatch;
/**
* Created by massivestars on 2018/3/10.
*/
public class ThreadLocalTest {
static ThreadLocal<String> tl = new ThreadLocal<String>();
/**
* 使用CountDownLatch使得三个线程都设置完值后再打印输出
*/
static CountDownLatch latch = new CountDownLatch(3);
static class SimpleThread extends Thread {
public SimpleThread(String threadName) {
super(threadName); //将线程的名称设为val
}
public void run() {
String threadName = Thread.currentThread().getName();
tl.set(threadName);
System.out.println(threadName + " has set value");
try {
latch.countDown();
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(threadName + " threadLocal value is: " + tl.get());
}
}
public static void main(String[] args) throws InterruptedException {
String currentThreadName = Thread.currentThread().getName();
tl.set(currentThreadName);
System.out.println(currentThreadName + " has set value");
latch.countDown();
Thread thread1 = new SimpleThread("thread1");
Thread thread2 = new SimpleThread("thread2");
thread1.start();
thread2.start();
latch.await();
System.out.println(currentThreadName + " threadLocal value is: " + tl.get());
}
}
示例程序一共有三个线程,分别为主线程main、线程thread1和thread2,三个线程根据线程名字给ThreadLocal设置值,然后用CountdownLatch阻塞各个线程直至三个线程都设置Theadlocal完毕 。运行后的输出结果如下, 三个线程内部输出的ThreadLocal值都和线程的名字一样,由于可以看出ThreadLocal在每个线程都有一个副本,互不影响。
csharp
main has set value
thread1 has set value
thread2 has set value
thread1 threadLocal value is: thread1
thread2 threadLocal value is: thread2
main threadLocal value is: main
实现细节
ThreadLocal
最重要的两个方法是get()
和set()
,所以查阅源码自然而然从这两方法入手,查看源码可以看出ThreadLocal
实际上是取当前线程的成员变量 threadLocals
进行存取,threadLocals
的类型为ThreadLocalMap
。
get()方法
java
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
//Thread t传参值为当前线程
//由此处可看出ThreadLocalMap变量是当前线程的成员变量
return t.threadLocals;
}
Thread类的ThreadLocalMap threadLocals 成员变量
java
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap
的定义如源码所示,是ThreadLocal
的内部类,实现了一个自定义的Map数据结构。
java
static class ThreadLocalMap {
//ThreadLocalMap真正的存储数据结构是Entry数组
private Entry[] table;
//Entry的定义
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
//key为弱引用
super(k);
value = v;
}
}
}
ThreadLocal
的set
方法实际上是调用当前线程ThreadLocalMap
的set方法,而实际存储的Entry
里,下标为Threadlocal
的this对象里threadLocalHashCode
的低位值,由此可知,一个线程可以有多个ThreadLocal
变量,每个ThreadLocal
变量对应一个Entry
。
java
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.
//上面提过ThreadLocal的真正存储是一个Entry数组
Entry[] tab = table;
int len = tab.length;
//len为Entry数组的长度,为2的N次方幂,len-1的值转为二进制是高位值取0,低位取1
//所以key.threadLocalHashCode & (len-1) 的值实际上是key.threadLocalHashCode的值取低位
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
//若e不为null,则该下标已被占用
//下标被占(hash冲突会出现这情况)后循环取下一个值(+1)直至没有被占用
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//k之前已被设置,用新值替换旧值
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
java
/**
* Increment i modulo len.
* 注: i + 1大于等于len时则从0开始
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
实际上ThreadLocal
只有一个非静态成员量,就是threadLocalHashCode
,threadLocalHashCode的低位是作为ThreadLocalMap
实际存储数据的Entry数组的下标, 每初始化一个ThreadLocal实例就要给threadLocalHashCode
赋值,从源码可看出threadLocalHashCode的值是AtomicInteger + HASH_INCREMENT(0x61c88647)。所以JVM
里第一个初始化的ThreadLocal
实例的threadLocalHashCode
的值为new AtomicInteger()
的默认值0,第二个ThreadLocal
实例的threadLocalHashCode
的值为0x61c88647 , 第三个为 0x61c88647 + 0x61c88647 , 依此类推。至于为什么用0x61c88647累加我们在后面作简单分析。
注意: 1、AtomicInteger 是静态变量 2、AtomicInteger#getAndAdd 是计算当前值相加所传参数的值,但返回的是当前值
java
/**
* ThreadLocals rely on per-thread linear-probe hash maps attached
* to each thread (Thread.threadLocals and
* inheritableThreadLocals). The ThreadLocal objects act as keys,
* searched via threadLocalHashCode. This is a custom hash code
* (useful only within ThreadLocalMaps) that eliminates collisions
* in the common case where consecutively constructed ThreadLocals
* are used by the same threads, while remaining well-behaved in
* less common cases.
*/
private final int threadLocalHashCode = nextHashCode();
/**
* The next hash code to be given out. Updated atomically. Starts at
* zero.
*/
private static AtomicInteger nextHashCode =new AtomicInteger();
/**
* The difference between successively generated hash codes - turns
* implicit sequential thread-local IDs into near-optimally spread
* multiplicative hash values for power-of-two-sized tables.
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* Returns the next hash code.
*/
private static int nextHashCode() {
//getAndAdd 是计算当前值相加所传参数的值,但返回的是当前值
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
get()方法
java
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
//this为ThreadLocal自身
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
//ThreadLocalMap#getEntry()
private Entry getEntry(ThreadLocal<?> key) {
//从上一调用栈帧可知ThreadLocal<?> key是ThreadLocal自身(this)
//key.threadLocalHashCode & (table.length - 1)实际上是取key.threadLocalHashCode的低位
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
//由set知道由于不同的TheadLocal发生下标冲突时会取nextIndex
//所以在get方法中i没有命中时也要继续继续遍历nextInt
return getEntryAfterMiss(key, i, e);
}
/**
*
* 当使用i取table[i]没命中时会继续遍历Entry[] table
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
//由于Entry[] table的元素数量达到阀值后会自动扩容
//也就是说一定会有Entry的key为null,所以while不会无限循环
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
//清除无效Entry(key为null)
expungeStaleEntry(i);
else
//注:若nextIndex大于等于len会从0开始
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
expungeStaleEntry
方法清除下标为staleSlot的entry,并且清除过期和重新分配发生过hash冲突导致下标位移的entry
java
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
//清除下标等于staleSlot的Entry
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
//rehash直到entry为null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
//当k不为null且计算出的下标值和i不相等时证明在set时已经发生过冲突
//此处将此entry重新设置在数组中的位置
if (h != i) {
tab[i] = null;
//.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
关于0x61c88647
从上面的代码分析可以看出ThreadLocal
实例里面threadLocalHashCode
的值为JVM里该ThreadLocal
实例化次序进行累加0x61c88647
,每实例化一个ThreadLocal,就是在原值的基础上加上0x61c88647 。为什么要用0x61c88647 这数字呢,该数字有什么魔力?ThreadLocal用到这个数值称为魔数,是为了让哈希码能均匀的分布在2的N次方的数组里(即Entry[] table),均匀分布的好处显然而见,能减少数组的下标探测,提高性能。 0x61c88647 这个数字的选取与斐波那契散列有关,0x61c88647 对应的十进制为1640531527
。斐波那契散列的值为2的32次方再乘以黄金分割数0.618 , 黄金分割数的计算方法为公式**(√5+1)÷2**, 所以这个魔数为**(int)(long)((1L << 32) * ((Math.sqrt(5) - 1)/2)),得到 -1640531527**, -1640531527 是1640531527 的无符号整数,亦即0x61c88647
。
下面我们用一个实验证实上面的理论
java
package org.encyclopedia.thread;
/**
* Created by massivestars on 2018/3/11.
*/
public class HashIncrement {
private static final int HASH_INCREMENT = 0x61c88647;
public static void hashCodeVal(int len) {
int nextHashCode = 0;
for (int i = 0; i < len; i++) {
System.out.print((nextHashCode & len -1) + " ");
nextHashCode = nextHashCode + HASH_INCREMENT;
}
System.out.print("\n");
}
public static void main(String[] args) {
hashCodeVal(16);
hashCodeVal(32);
}
}
程序输出如下,可见使用0x61c88647
这个确实能更均匀的分布在即Entry[] table数组里。
markdown
0 7 14 5 12 3 10 1 8 15 6 13 4 11 2 9
0 7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25
关于ThreadLocal的内存泄漏
每个thread 中都存在一个ThreadLocalMap , ThreadLocalMap中的key为一个threadlocal 实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被GC回收.
我们再来看看ThreadLocal的引用链: Thread -> ThreadLocalMap -> Entry -> value
虽然ThreadLocal
的key作为弱引用会在每次GC 的时候回收,从而key变为null,然而线程没有结束的话,上面的引用链还是会存在。ThreadLocal对这个情况也是作了很大优化,在每次get() 、set()的时候都会遍历把key为null的Entry清除。所以在绝大部分情况都不会存在内存泄漏的情况,但要注意到在get()、set()的操作间隔且key还没有被GC 时value存储大内存对象的情况,若多个线程都各自持有大内存value且线程没有被收回则很容易出现内存溢出。所在我们在每次使用ThreadLocal 对象后最好调用remove()
方法显式清除value。
特别需要注意使用线程池的时候,线程结束是不会销毁的,会再次使用的,会出现读脏数据和内存泄露的情况。
最佳实践
-
ThreadLocal
对象使用static修饰。否则每个线程对于每个使用的非静态ThreadLocal
实例都初始化一个ThreadLocal
实例。由于每个线程都有自己的副本,ThreadLocal
只是作一个key 的存在,使用static静态修饰创建一个实例即可。 -
每次使用
ThreadLocal
对象结束后调用remove()
方法清除key和value。