多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。
ThreadLocal 是 JDK 包提供的,它提供线程本地变量,如果创建了ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题,如下图所示
ThreadLocal简单使用此类提供线程局部变量。这些变量与它们的普通对应变量不同,因为每个访问一个的线程(通过其 {@code get} 或 {@code set} 方法)都有自己的、独立初始化的变量副本。 {@code ThreadLocal} 实例通常是希望将状态与线程相关联的类中的私有静态字段(例如,用户 ID 或事务 ID)。
只要线程处于活动状态并且 {@code ThreadLocal} 实例是可访问的,每个线程都持有对其线程局部变量副本的隐式引用;线程消失后,它的所有线程本地实例副本都将进行垃圾回收(除非存在对这些副本的其他引用)
下面的例子中,开启两个线程,在每个线程内部设置了本地变量的值,然后调用 print 方法打印当前本地变量的值。如果在打印之后调用本地变量的 remove 方法会删除本地内存中的变量,代码如下所示
package com.kelecc.demo.thead;
public class TheadLocalTest {
public static ThreadLocal local = new ThreadLocal();
public static void print(String str){
//打印当前线程中本地内存中本地变量的值
System.out.println(str+":"+local.get());
//清除本地内存中的本地变量
local.remove();
}
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
public void run() {
local.set("localVal1");
print("thread1");
//打印本地变量
System.out.println("after remove : " + local.get());
}
});
Thread t2 = new Thread(new Runnable() {
public void run() {
//设置线程1中本地变量的值
local.set("localVar2");
//调用打印方法
print("thread2");
//打印本地变量
System.out.println("after remove : " + local.get());
}
});
t1.start();
t2.start();
}
}
ThreadLocal的实现原理
其中关键就在 get 和 set 方法 (方法在 TheadLocal.class 199行)
set方法 public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取存储数据结构类型
ThreadLocalMap map = getMap(t);
//如果map不存在,就创建,否则就set
if (map != null){
map.set(this, value);
}else{
createMap(t, value);
}
}
ThreadLocalMap getMap(Thread t) {
//thread中维护了一个ThreadLocalMap
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
//实例化一个新的Map,并赋值给线程的成员变量threadLocals
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
每个线程持有一个 ThreadLocalMap 对象。每一个新的线程 Thread 都会实例化一个 ThreadLocalMap并赋值给成员变量 threadLocals,使用时若已经存在 threadLocals 则直接使用已经存在的对象。(声明变量在 Thead.class 182行)
ThreadLocal.ThreadLocalMap threadLocals = null;
接下来看createMap方法中的实例化过程( ThreadLocalMap.class为 TheadLocal静态内部类,298行)
static class ThreadLocalMap {
static class Entry extends WeakReference> {
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
.......
private int threshold; // Default to 0
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) {
//内部成员数组,INITIAL_CAPACITY值为16的常量
table = new Entry[INITIAL_CAPACITY];
//位运算,结果与取模相同,计算出需要存放的位置
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
}
在实例化 ThreadLocalMap 时创建了一个长度为 16 的 Entry 数组。
通过 hashCode与length位运算确定出一个索引值 i,这个 i 就是被存储在 table 数组中的位置。
每个线程 Thread 持有一个 ThreadLocalMap 类型的实例 threadLocals,结合此处的构造方法可以理解成每个线程 Thread 都持有一个 Entry 型的数组 table,而一切的读取过程都是通过操作这个数组 table完成的。
ThreadLocalMap 中的 Entry 的 key 使用的是 ThreadLocal对象的弱引用,在没有其他地方对 ThreadLocal 依赖,ThreadLocalMap 中的 ThreadLocal 对象就会被回收掉,但是对应的不会被回收,这个时候 Map 中就可能存在 key 为 null 但是 value 不为 null 的项,这需要实际的时候使用完毕及时调用 remove 方法避免内存泄漏。
每个线程 Thread 持有一个 ThreadLocalMap 类型的实例 threadLocals,结合此处的构造方法可以理解成每个线程 Thread 都持有一个 Entry 型的数组 table,而一切的读取过程都是通过操作这个数组table 完成的
那么 table 是 set 和 get 的方法就是我们重点看的,我们先看下如下代码:
//在某一线程声明了ABC三种类型的ThreadLocal ThreadLocal t1 = new ThreadLocal(); ThreadLocal t2 = new ThreadLocal(); ThreadLocalt3 = new ThreadLocal ();
由前面我们知道对于一个 Thread 来说只有持有一个 ThreadLocalMap,所以 ABC 对应同一个 ThreadLocalMap 对象。为了管理 ABC,于是将他们存储在一个数组的不同位置,而这个数组就是上面提到的 Entry 型的数组 table。
那么问题来了,ABC 在 table 中的位置是如何确定的?为了能正常够正常的访问对应的值,肯定存在一种方法计算出确定的索引值i(代码 454行)
private void set(ThreadLocal> key, Object value) {
// 我们不像 get() 那样使用快速路径,因为使用 set() 创建新条目至少与替换现有条目一样常见,在这种情况下,快速路径会经常失败.
Entry[] tab = table;
int len = tab.length;
//获取索引值,这个地方是比较特别的地方
int i = key.threadLocalHashCode & (len-1);
//遍历tab如果已经存在则更新值
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//如果上面没有遍历成功则创建新值
tab[i] = new Entry(key, value);
int sz = ++size;
//满足条件数组扩容x2
if (!cleanSomeSlots(i, sz) && sz >= threshold)
//刷新table,
rehash();
}
看到 int i = key.threadLocalHashCode & (len-1); 此处源码 85行开始;
简而言之就是将 threadLocalHashCode 进行一个位运算(取模)得到索引 i,threadLocalHashCode代码如下。
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode =
new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
因为 static 的原因,在每次 new ThreadLocal 时因为 threadLocalHashCode 的初始化,会使 threadLocalHashCode 值自增一次,增量0x61c88647。
0x61c88647 是斐波那契散列乘数,它的优点是通过它散列 (hash) 出来的结果分布会比较均匀,可以很大程度上避免 hash 冲突,已初始容量 16 为例,hash 并与 15 位运算计算数组下标结果如下:
| hashCode | 数组下标 |
|---|---|
| 0x61c88647 | 7 |
| 0xc3910c8e | 14 |
| 0x255992d5 | 5 |
| 0x8722191c | 12 |
| 0xe8ea9f63 | 3 |
| 0x4ab325aa | 10 |
| 0xac7babf1 | 1 |
| 0xe443238 | 8 |
| 0x700cb87f | 15 |
总结:
- 对于某一ThreadLocal 来讲,他的索引值 i 是确定的,在不同线程之间访问时访问的是不同的 table 数组的同一位置即都为 table [i],只不过这个不同线程之间的 table 是独立的。
- 对于同一线程的不同 ThreadLocal来讲,这些 ThreadLocal实例共享一个 table 数组,然后每个 ThreadLocal实例在 table 中的索引 i 是不同的。
- 每个线程都对应一个 ThreadLocalMap, 而threadlocal负责访问和维护 ThreadLocalMap.
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获得一个 ThreadLocalMap
ThreadLocalMap map = getMap(t);
//ThreadLocalMap,不是空就返回table里面的值;
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//创建一个新的ThreadLocalMap;
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
//此处set实现
return t.threadLocals;
}
private T setInitialValue() {
//声明一个泛型变量
T value = initialValue();
//获取一个当前线程
Thread t = Thread.currentThread();
//获取一个ThreadLocalMap
ThreadLocalMap map = getMap(t);
//如果Map不是空的就设置值,否则创建一个新的TheadLocalMap
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
ThreadLocal 特性
ThreadLocal和 Synchronized 都是为了解决多线程中相同变量的访问冲突问题,不同的点是
- Synchronized 是通过线程等待,牺牲时间来解决访问冲突
- ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且相比于 Synchronized,ThreadLocal 具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。
正因为 ThreadLocal 的线程隔离特性,使他的应用场景相对来说更为特殊一些。在 android 中 Looper、ActivityThread 以及 AMS 中都用到了 ThreadLocal。当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用 ThreadLocal。
ThreadLocal 不支持继承性同一个 ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。(threadLocals 中为当前调用线程对应的本地变量,所以二者自然是不能共享的)
package com.kelecc.mybatis.thead;
public class ThreadLocalTest2 {
//创建线程本地变量
public static ThreadLocal threadLocal = new ThreadLocal();
public static void main(String[] args) {
//在main线程中添加main线程的本地变量
threadLocal.set("mainVal");
//新创建一个子线程
Thread thread = new Thread(new Runnable() {
public void run() {
System.out.println("子线程中的本地变量值:"+threadLocal.get());
}
});
thread.start();
//输出main线程中的本地变量值
System.out.println("mainx线程中的本地变量值:"+threadLocal.get());
}
}
ThreadLocal 使用不当的内存泄漏问题
ThreadLocal 只是一个工具类,他为用户提供 get、set、remove 接口操作实际存放本地变量的 threadLocals(调用线程的成员变量),也知道 threadLocals是一个 ThreadLocalMap 类型的变量。
此处说下Java 中的四种引用类型;
-
强引用:Java 中默认的引用类型,一个对象如果具有强引用那么只要这种引用还存在就不会被 GC;
-
软引用:简言之,如果一个对象具有弱引用,在JVM发生 OOM之前(即内存充足够使用),是不会 GC这个对象的;只有到 JVM内存不足的时候才会 GC掉这个对象。软引用和一个引用队列联合使用,如果软引用所引用的对象被回收之后,该引用就会加入到与之关联的引用队列中;
-
弱引用(这里讨论 ThreadLocalMap 中的 Entry 类的重点):如果一个对象只具有弱引用,那么这个对象就会被垃圾回收器GC掉 ( 被弱引用所引用的对象只能生存到下一次 GC之前,当发生 GC 时候,无论当前内存是否足够,弱引用所引用的对象都会被回收掉 )。弱引用也是和一个引用队列联合使用,如果弱引用的对象被垃圾回收期回收掉,JVM 会将这个引用加入到与之关联的引用队列中。若引用的对象可以通过弱引用的 get 方法得到,当引用的对象被回收掉之后,再调用 get 方法就会返回 null;
-
虚引用:虚引用是所有引用中最弱的一种引用,其存在就是为了将关联虚引用的对象在被 GC 掉之后收到一个通知。(不能通过 get 方法获得其指向的对象)
看此处代码
static class Entry extends WeakReference> { Object value; //k:ThreadLocal的引用,被传递给WeakReference的构造方法 Entry(ThreadLocal> k, Object v) { super(k); value = v; } } } public class WeakReference extends Reference { public WeakReference(T referent) { //调用父类构造方法 super(referent); } public WeakReference(T referent, ReferenceQueue super T> q) { //调用父类构造方法 super(referent, q); } } public abstract class Reference { //Reference构造方法 Reference(T referent) { this(referent, null); } Reference(T referent, ReferenceQueue super T> queue) { this.referent = referent; this.queue = (queue == null) ? ReferenceQueue.NULL : queue; } }
总结:
- 当前 ThreadLocal 的引用 k 被传递给 WeakReference的构造函数,所以 ThreadLocalMap 中的 key 为ThreadLocal 的弱引用。
- 当一个线程调用 ThreadLocal 的 set 方法设置变量的时候,当前线程的 ThreadLocalMap 就会存放一个记录,这个记录的 key 值为 ThreadLocal 的弱引用,value 就是通过 set 设置的值。
- 如果当前线程一直存在且没有调用该 ThreadLocal 的 remove 方法,如果这个时候别的地方还有对 ThreadLocal 的引用,那么当前线程中的 ThreadLocalMap 中会存在对 ThreadLocal变量的引用和 value 对象的引用,是不会释放的,就会造成内存泄漏。
- 考虑这个 ThreadLocal变量没有其他强依赖,如果当前线程还存在,由于线程的ThreadLocalMap里面的 key 是弱引用,所以当前线程的 ThreadLocalMap 里面的 ThreadLocal变量的弱引用在 gc 的时候就被回收,但是对应的 value 还是存在的这就可能造成内存泄漏 (因为这个时候 ThreadLocalMap 会存在 key 为 null 但是 value 不为 null 的 entry 项)。
- THreadLocalMap 中的 Entry 的 key 使用的是 ThreadLocal 对象的弱引用,在没有其他地方对 ThreadLocal 依赖,ThreadLocalMap 中的 ThreadLocal对象就会被回收掉,但是对应的不会被回收,这个时候 Map 中就可能存在 key 为 null 但是 value 不为 null 的项,这需要实际的时候使用完毕及时调用 remove 方法避免内存泄漏。



