@pastqing
2016-04-13T15:41:20.000000Z
字数 8111
阅读 2906
java
ThreadLocal就是线程局部变量, ThreadLocal不是线程,它是线程的一个本地化对象。当工作于多线程的对象使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供一个这个变量的副本。因此每个线程都可以独立使用自己的变量, 而不用管其他线程。
ThreadLocal对外提供了4个方法, 他们的作用如下:
protected T initialValue()
: 返回该线程局部变量的初始值。该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
public T get()
: 返回当前线程所对应的线程局部变量的副本, 如果如果当前线程该变量没有赋值, 它将被initialValue
方法初始化。
public void set(T value)
: 设置当前线程所对应的线程局部变量的值。
public void remove()
: 删除当前线程所对应的线程局部变量的值。如果删除后,这个线程局部变量被该线程读取, 会再次调用initialValue
方法初始化这个变量。当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
一个使用ThreadLocal的例子:我之前在Servlet的线程安全性中给出解决方案来避免出现多线程共享变量的问题, 现在我们用**ThreadLocal来解决此问题。
public class ThreadSafe extends HttpServlet {
//定义一个实例变量count
private ThreadLocal<Integer> counter = new ThreadLocal<Integer>() {
protected Integer initialValue() {
return 0;
}
};
protected void doGet(
HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException {
doPost(request, response);
protected void doPost(
HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException {
//输出10个count计数
//输出当前线程名
response.getWriter().println(Thread.currentThread().getName() + ": <br>");
for(int i = 0; i < 10; i++) {
response.getWriter().println("Counter = " + counter.get() + "<br>");
try {
Thread.sleep(1000);
counter.set(counter.get()+1);
} catch (InterruptedException exc) {}
}
}
这里我们用一个ThreadLocal来装载Integer变量, 并采用匿名内部类重写其initialValue
方法, 从而解决了线程安全性问题。
下面对ThreadLocal源码进行分析, 深入了解它的实现方式。
从整个ThreadLocal类中可知, ThreadLocal维护了3个int型的成员变量和一个内部静态类, 下面我们将分模块分析一下源码:
ThreadLocal有三个int型的实例变量, 这三个实例变量的作用是用来生成和维护其hashcode, 它们如下定义:
public class ThreadLocal<T> {
//表示当前ThreadLocal的hashcode值
private final int threadLocalHashCode = nextHashCode();
//静态初始化一个hashcode值
private static AtomicInteger nextHashCode = new AtomicInteger();
//表示了连续分配的两个ThreadLocal实例的threadLocalHashCode值的增量, 这是一个常量
private static final int HASH_INCREMENT = 0x61c88647;
}
ThreadLocal的hashcode是用于其内部类ThreadLocalMap的, 具体后文会讲到。这里说一下此处hashcode的生成策略:
所有的ThreadLocal对象都共享一个AtomicInteger nextHashCode, 因此将其用于计算hashcode, 初始化为0;
nextHashCode
方法是用来计算下一个hashcode初始值, 以静态常量0x61c88647作为递增。即为nextHashCode += HASH_INCREMENT;
计算hashcode的算法为: ThreadLocal.threadLocalHashCode & (table.length - 1)
。以下是几点说明:
ThreadLocalMap可以简单的理解为一个Map, 这个Map的作用是给每一个线程都会存放一个线程局部变量在其中。我们打开Thread源码看一下:
public class Thread implements Runnable {
...
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
}
通过这个Map我们就可以去获取ThreadLocal里存的线程局部变量。
下面看一下ThreadLocalMap的源码:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal> {
/** The value associated with this ThreadLocal. */
Object value;
//可见Entry封装了ThreadLocal的弱引用, 以及线程局部变量
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
}
ThreadLocalMap中又封装了一个内部静态类Entry。封装出来一个Entry的作用是表示ThreadLocalMap中的每一项。同时Entry继承了WeakReference, 这是为了将ThreadLocal指明为弱引用, 这样通过==null
来判断是否还有具有ThreadLocal的引用了。
//table的默认初始化容量, 总是2的n次方
private static final int INITIAL_CAPACITY = 16;
//一个Entry数组, 数组长度总是2的n次方
private Entry[] table;
//table中entry的数量
private int size = 0;
//下一次扩容的大小
private int threshold; // Default to 0
接下来看一下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);
}
构造函数非常容易理解, 就是初始化了table, 根据hash算法生成一个hansh值, 然后填入一个新的Entry实例。setThreshold(int len)
方法的作用是设置负载因子threshold, 此处表示当容量达到2/3时就进行扩容(resize)。
这是ThreadLocalMap另一个构造函数, 根据ThreadLocalMap构造:
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
ThreadLocal key = e.get();
//此处key为null的话,即表示ThreadLocal已不存在
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
//如果此位置已有元素,轮询下一个(i+1)位置
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
以上用ThreadLocalMap作为构造方法,思路也很简单。就是根据老的ThreadLocalMap创建一个新的。注意一下注释部分即可。
上文简单讲解了ThreadLocalMap。我们从当前工作线程中获取其线程局部变量的Value值时, 实际上是从ThreadLocalMap中获取的。在每个Thread中都维护着ThreadLocalMap, 下面是Thread部分源码:
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
根据ThreadLocal api,我们知道获取ThreadLocal的变量是调用其get方法, 下面是get方法的源码:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
当调用get
方法时, 会首先获得当前的线程的threadLocals。如果这个threadLocals已经创建并初始化过了,就从中层层取出Value返回给用户。否则,则调用setInitialValue
创建map并且进行初始化。下面是setInitialValue
方法的源码:
private T setInitialValue() {
//initialValue可以被重载, 用来设置线程局部变量的初始值,默认为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;
}
从当前线程中获取ThreadLocalMap, 如果map不为空,就调用set
方法。否则调用createMap
创建一个新的Map。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
下面看一下set
方法的具体实现:
private void set(ThreadLocal key, Object value) {
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)]) {
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;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
从set
方法的实现可以了解到ThreadLocalMap的set策略:
遍历所有Entry, 找到对应存在的则不set, 直接返回。如果未找到, 发现Entry有不存在的key, 则调用replaceStaleEntry
替换之。遍历完一遍后无果, 在new一个新的Entry。
关于cleanSomeSlots
是ThreadLocal的内存回收过程, 后文会详细讲解。
以上就是ThreadLocal, get的调用过程,其set方法的调用过程与get方法非常类似,这里就不重复讲解了。
在说ThreadLocal内存回收问题之前, 首先看看ThreadLocal的生命周期中都存在哪些引用。
实现代表强引用, 虚线代表弱引用。
每个Thread中都存在一个map,map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例。这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal.当把threadlocal实例tl置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收
注意: 以上的这个图以及这段文字, 我是参考和引用这篇文章的
如果线程可以活很长的时间,并且该线程保存的线程局部变量有很多(也就是 Entry 对象很多),那么就涉及到在线程的生命期内如何回收 ThreadLocalMap 的内存了,不然的话,Entry对象越多,那么ThreadLocalMap 就会越来越大,占用的内存就会越来越多,所以对于已经不需要了的线程局部变量,就应该清理掉其对应的Entry对象。使用的方式是,Entry对象的key是WeakReference 的包装。当ThreadLocalMap 的 private Entry[] table,已经被占用达到了三分之二时 threshold = 2/3(也就是线程拥有的局部变量超过了10个) ,就会尝试回收 Entry 对象,我们可以看到 ThreadLocalMap.set方法中有下面的代码:
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
下面看一下cleanSomeSlots
源码:
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
cleanSomeSlots
方法从源码上看就是找出废弃的Entry并清除。其中查找Entry的过程却执行了log2(n)次。这样做的目的是提高效率。
下面是expungeStaleEntry(int i)
方法源码:
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;
}
expungeStaleEntry
的作用清除无用Entry,以及解决在无用Entry之间与下一个可用空间之间的hash冲突问题
至于rehash()
方法源代码中的注释也说得很明白, 即先清除一遍无用Entry, 再扩容。
/**
* Re-pack and/or re-size the table. First scan the entire
* table removing stale entries. If this doesn't sufficiently
* shrink the size of the table, double the table size.
*/
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
参考文献:深入JDK源码之ThreadLocal类