1.ThreadLocal内部结构
下图是TreadLocal的基本数据结构:
图1-ThreadLocal数据结构图
其中对外公开的主要方法:
图2-ThreadLocal外部可见方法
我们使用该类时,一般是将该类作为某个类的静态成员变量重写其initialValue()方法,设置线程变量的初始值,以下是其的简单使用:
public class ThreadLocalDemo {
private Integer count;
public ThreadLocalDemo(Integer i){
this.count=i;
}
//ThreadLocal的简单使用
private static final ThreadLocal<ThreadLocalDemo> HOLDERS = new ThreadLocal<ThreadLocalDemo>() {
@Override
protected ThreadLocalDemo initialValue() {
return new ThreadLocalDemo(0);
}
};
/**
* 每次get的时候计数增加1;
*/
public static ThreadLocalDemo getDemo(){
ThreadLocalDemo demo=HOLDERS.get();
demo.count++;
return demo;
}
public Integer getCount(){
return this.count;
}
}
在多线程场景下使用该demo类:
package com.xin.thread;
/**
* 多线程测试案例
*/
public class TestDemo {
public static void main(String []args){
new Thread(new Runnable() {
@Override
public void run() {
int i=0;
while(i<5) {
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalDemo.getDemo().getCount());
i++;
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
int i=0;
while(i<5) {
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalDemo.getDemo().getCount());
i++;
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
int i=0;
while(i<5) {
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalDemo.getDemo().getCount());
i++;
}
}
}).start();
}
}
执行结果如下所示:
可以看到在多个线程同时使用ThreadLocalDemo.getDemo()方法的时候并没有发生我们可能预料的竞争,同一个线程内总是按顺序取得了计数值,不同线程间也没有发生争夺计数值的情况,不同线程总是独立执行的。似乎在demo类中定义的类静态变量的功能失效了,我们知道通常情况下,类变量(与成员变量相区分)总是被所有线程共享的。
这里的疑惑之处在于:HOLDERS.get()
在没有给共享变量holder上锁的情况下,并没有产生线程之间竞争,进去这个方法可以看到:
public T get() {
Thread t = Thread.currentThread();//1获取当前线程
ThreadLocalMap map = getMap(t);//2,Thread类中存在ThreadLocal.ThreadLocalMap threadLocals = null的定义,通过该方法获取ThreadLocalMap,该类是TreadLocal类中的一个内部类,存储了
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();//3
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
//创建ThreadLocalMap,该map以键值对的方式存储了当前线程与泛型变量;
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
这里涉及到ThreadLocal的内部类ThreadLocalMap以及ThreadLocalMap的内部类Entry,由下图可知Entry主要是存储Treadlocal实例弱引用与变量值对的数据结构;
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocalMap内部构造如下图所示:
这里我们重点关注其构造方法:
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
在ThreadLocalMap中含由Entry[] table数组,在新建ThreadLocalMap实例中会将当前ThreadLocal弱引用和变量值存储在该数组中。
回到ThreadLocal的get()方法:
public T get() {
Thread t = Thread.currentThread();//1获取当前线程
ThreadLocalMap map = getMap(t);//2,Thread类中存在ThreadLocal.ThreadLocalMap threadLocals = null的定义,通过该方法获取ThreadLocalMap,该类是TreadLocal类中的一个内部类,存储了
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();//3
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
由于线程类中含有ThreadLocal.ThreadLocalMap threadLocals = null;
属性,当根据当前线程获取该map的值为空(步骤1,2)时会创建该map(步骤3),当同一线程下一次获取类型为T的变量时,会在其threadLocals中查找是否存在与该线程对应的变量,直接以当前线程为key查找,若存在返回其值,可见用ThreadLocal包装的变量,每个线程在获取时就已经将其存储在自己的属性threadLocals中。有多个被ThreadLocal包装的变量时,同理都存储在该属性中。
关系描述可以用下图描述:
每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例的弱引用,value是真正需要存储的Object。ThreadLocalMap是使用ThreadLocal的弱引用作为Key的,弱引用的对象在GC时会被回收。https://zhuanlan.zhihu.com/p/28049662
对于多个ThreadLocal包装的变量,其关系如下图所示:
可见该类的使用在多线程环境中是典型的以空间换时间。
2.为什么使用ThreadLocal?
在使用ThreadLocal时,发现使用该类封装的变量与在各线程中使用局部变量完全可以达到相同的效果,为什么还会使用ThreadLocal呢?先看代码:
package threadlocaldemo;
public class ThreadLocalDemo {
private static ThreadLocal<Integer>contextValue=new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return 0;
}
};
public Integer getCount(){
int count=contextValue.get();
count++;
contextValue.set(count);
return count;
}
public static void main(String []args){
ThreadLocalDemo demo=new ThreadLocalDemo();
new Thread(new Runnable() {
@Override
public void run() {
int a0=1;
while(true){
System.out.println(Thread.currentThread().getName()+demo.getCount());
System.out.println("a0:"+(a0++));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
int a1=1;
while(true) {
System.out.println(Thread.currentThread().getName()+demo.getCount());
System.out.println("a1:"+(a1++));
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
int a2=1;
while(true) {
System.out.println(Thread.currentThread().getName()+demo.getCount());
System.out.println("a2:"+(a2++));
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
其中contextValue为ThreadLocal封装的变量,a0,a1,a2分别是线程局部变量,输出结果如下:
Thread-0:1
a0:1
Thread-1:1
a1:1
Thread-2:1
a2:1
Thread-0:2
a0:2
Thread-1:2
a1:2
Thread-2:2
a2:2
Thread-0:3
a0:3
Thread-1:3
a1:3
Thread-2:3
a2:3
Thread-1:4
a1:4
Thread-2:4
Thread-0:4
a0:4
a2:4
Thread-2:5
a2:5
Thread-0:5
a0:5
Thread-1:5
a1:5
Thread-1:6
a1:6
Thread-0:6
a0:6
可以看到,两者效果一致,那么为什么还需要去使用ThreadLocal呢?我们写代码不能只是从效用上去考虑,我们还应该考虑代码的易读性,可维护性等因素;从上面的案例我们可以看到,同样的效果,一旦修改代码,ThreadLocal变量的使用和线程局部变量的使用显然不是一个体量。另外,从官方描述中可以看到,该类主要是为方便线程处理自己的状态而设立的,每个线程在获取该类变量的时候,都拥有它自己相对独立的变量初始化拷贝。
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).
一个ThreadLocal变量可以理解为全局变量,每个线程都只能读写自己线程的独立副本,互不干扰,解决了参数在一个线程中各个函数之间互相传递的问题。
3.使用ThreadLocal类存在的问题
前面我们有说到,每个线程对ThreadLocal实例的引用是弱引用的key,而弱引用的对象的虚拟机垃圾回收的时候将会被回收,但是value值是被ThreadLocalMap强引用的,其生命周期与线程的生命周期相同。那么问题来了,如果value值很大,程序的线程本地变量使用较多,就会造成因无法及时回收而造成outofmemory的内存泄漏。庆幸的是,ThreadLocal为我们提供了remove()方法:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
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) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
//通过重新散列位于staleSlot和下一个null插槽之间的任何可能冲突的条目来清除陈旧的条目。 这还会清除尾随null之前遇到的所有其他过时的条目。
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter 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);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
该方法逻辑很简单,通过往后环形查找到与指定key相同的entry后,先通过clear方法将key置为null后,使其转换为一个脏entry,然后调用expungeStaleEntry方法将其value置为null,以便垃圾回收时能够清理,同时将table[i]置为null。
4.参考文档
https://www.jianshu.com/p/30ee77732843