[关闭]
@pastqing 2016-04-13T07:41:20.000000Z 字数 8111 阅读 2768

ThreadLocal学习笔记

java


一、ThreadLocal是什么

ThreadLocal就是线程局部变量ThreadLocal不是线程,它是线程的一个本地化对象。当工作于多线程的对象使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供一个这个变量的副本。因此每个线程都可以独立使用自己的变量, 而不用管其他线程。

二、ThreadLocal使用方法

ThreadLocal对外提供了4个方法, 他们的作用如下:

一个使用ThreadLocal的例子:我之前在Servlet的线程安全性中给出解决方案来避免出现多线程共享变量的问题, 现在我们用**ThreadLocal来解决此问题。

  1. public class ThreadSafe extends HttpServlet {
  2. //定义一个实例变量count
  3. private ThreadLocal<Integer> counter = new ThreadLocal<Integer>() {
  4. protected Integer initialValue() {
  5. return 0;
  6. }
  7. };
  8. protected void doGet(
  9. HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException {
  10. doPost(request, response);
  11. protected void doPost(
  12. HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException {
  13. //输出10个count计数
  14. //输出当前线程名
  15. response.getWriter().println(Thread.currentThread().getName() + ": <br>");
  16. for(int i = 0; i < 10; i++) {
  17. response.getWriter().println("Counter = " + counter.get() + "<br>");
  18. try {
  19. Thread.sleep(1000);
  20. counter.set(counter.get()+1);
  21. } catch (InterruptedException exc) {}
  22. }
  23. }

这里我们用一个ThreadLocal来装载Integer变量, 并采用匿名内部类重写其initialValue方法, 从而解决了线程安全性问题。

三、ThreadLocal源码分析(JDK7)

下面对ThreadLocal源码进行分析, 深入了解它的实现方式。
ThreadLocal.png-192.1kB

从整个ThreadLocal类中可知, ThreadLocal维护了3个int型的成员变量和一个内部静态类, 下面我们将分模块分析一下源码:

1.ThreadLocal的hashcode生成原理

ThreadLocal有三个int型的实例变量, 这三个实例变量的作用是用来生成和维护其hashcode, 它们如下定义:

  1. public class ThreadLocal<T> {
  2. //表示当前ThreadLocal的hashcode值
  3. private final int threadLocalHashCode = nextHashCode();
  4. //静态初始化一个hashcode值
  5. private static AtomicInteger nextHashCode = new AtomicInteger();
  6. //表示了连续分配的两个ThreadLocal实例的threadLocalHashCode值的增量, 这是一个常量
  7. private static final int HASH_INCREMENT = 0x61c88647;
  8. }

ThreadLocalhashcode是用于其内部类ThreadLocalMap的, 具体后文会讲到。这里说一下此处hashcode的生成策略:

2.ThreadLocalMap详细讲解

ThreadLocalMap可以简单的理解为一个Map, 这个Map的作用是给每一个线程都会存放一个线程局部变量在其中。我们打开Thread源码看一下:

  1. public class Thread implements Runnable {
  2. ...
  3. /* ThreadLocal values pertaining to this thread. This map is maintained
  4. * by the ThreadLocal class. */
  5. ThreadLocal.ThreadLocalMap threadLocals = null;
  6. }

通过这个Map我们就可以去获取ThreadLocal里存的线程局部变量。
下面看一下ThreadLocalMap的源码:

  1. static class ThreadLocalMap {
  2. static class Entry extends WeakReference<ThreadLocal> {
  3. /** The value associated with this ThreadLocal. */
  4. Object value;
  5. //可见Entry封装了ThreadLocal的弱引用, 以及线程局部变量
  6. Entry(ThreadLocal k, Object v) {
  7. super(k);
  8. value = v;
  9. }
  10. }
  11. }

ThreadLocalMap中又封装了一个内部静态类Entry。封装出来一个Entry的作用是表示ThreadLocalMap中的每一项。同时Entry继承了WeakReference, 这是为了将ThreadLocal指明为弱引用, 这样通过==null来判断是否还有具有ThreadLocal的引用了。

  1. //table的默认初始化容量, 总是2的n次方
  2. private static final int INITIAL_CAPACITY = 16;
  3. //一个Entry数组, 数组长度总是2的n次方
  4. private Entry[] table;
  5. //table中entry的数量
  6. private int size = 0;
  7. //下一次扩容的大小
  8. private int threshold; // Default to 0

接下来看一下ThreadLocalMap的构造函数

  1. ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
  2. table = new Entry[INITIAL_CAPACITY];
  3. int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  4. table[i] = new Entry(firstKey, firstValue);
  5. size = 1;
  6. setThreshold(INITIAL_CAPACITY);
  7. }

构造函数非常容易理解, 就是初始化了table, 根据hash算法生成一个hansh值, 然后填入一个新的Entry实例。setThreshold(int len)方法的作用是设置负载因子threshold, 此处表示当容量达到2/3时就进行扩容(resize)。

这是ThreadLocalMap另一个构造函数, 根据ThreadLocalMap构造:

  1. private ThreadLocalMap(ThreadLocalMap parentMap) {
  2. Entry[] parentTable = parentMap.table;
  3. int len = parentTable.length;
  4. setThreshold(len);
  5. table = new Entry[len];
  6. for (int j = 0; j < len; j++) {
  7. Entry e = parentTable[j];
  8. if (e != null) {
  9. ThreadLocal key = e.get();
  10. //此处key为null的话,即表示ThreadLocal已不存在
  11. if (key != null) {
  12. Object value = key.childValue(e.value);
  13. Entry c = new Entry(key, value);
  14. int h = key.threadLocalHashCode & (len - 1);
  15. //如果此位置已有元素,轮询下一个(i+1)位置
  16. while (table[h] != null)
  17. h = nextIndex(h, len);
  18. table[h] = c;
  19. size++;
  20. }
  21. }
  22. }
  23. }

以上用ThreadLocalMap作为构造方法,思路也很简单。就是根据老的ThreadLocalMap创建一个新的。注意一下注释部分即可。

3.ThreadLocal的调用过程

上文简单讲解了ThreadLocalMap。我们从当前工作线程中获取其线程局部变量的Value值时, 实际上是从ThreadLocalMap中获取的。在每个Thread中都维护着ThreadLocalMap, 下面是Thread部分源码:

  1. ThreadLocal.ThreadLocalMap threadLocals = null;
  2. ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

根据ThreadLocal api,我们知道获取ThreadLocal的变量是调用其get方法, 下面是get方法的源码:

  1. public T get() {
  2. Thread t = Thread.currentThread();
  3. ThreadLocalMap map = getMap(t);
  4. if (map != null) {
  5. ThreadLocalMap.Entry e = map.getEntry(this);
  6. if (e != null)
  7. return (T)e.value;
  8. }
  9. return setInitialValue();
  10. }

当调用get方法时, 会首先获得当前的线程的threadLocals。如果这个threadLocals已经创建并初始化过了,就从中层层取出Value返回给用户。否则,则调用setInitialValue创建map并且进行初始化。下面是setInitialValue方法的源码:

  1. private T setInitialValue() {
  2. //initialValue可以被重载, 用来设置线程局部变量的初始值,默认为null
  3. T value = initialValue();
  4. Thread t = Thread.currentThread();
  5. ThreadLocalMap map = getMap(t);
  6. if (map != null)
  7. map.set(this, value);
  8. else
  9. createMap(t, value);
  10. return value;
  11. }

从当前线程中获取ThreadLocalMap, 如果map不为空,就调用set方法。否则调用createMap创建一个新的Map。

  1. void createMap(Thread t, T firstValue) {
  2. t.threadLocals = new ThreadLocalMap(this, firstValue);
  3. }

下面看一下set方法的具体实现:

  1. private void set(ThreadLocal key, Object value) {
  2. Entry[] tab = table;
  3. int len = tab.length;
  4. int i = key.threadLocalHashCode & (len-1);
  5. for (Entry e = tab[i];
  6. e != null;
  7. e = tab[i = nextIndex(i, len)]) {
  8. ThreadLocal k = e.get();
  9. if (k == key) {
  10. e.value = value;
  11. return;
  12. }
  13. if (k == null) {
  14. replaceStaleEntry(key, value, i);
  15. return;
  16. }
  17. }
  18. tab[i] = new Entry(key, value);
  19. int sz = ++size;
  20. if (!cleanSomeSlots(i, sz) && sz >= threshold)
  21. rehash();
  22. }

set方法的实现可以了解到ThreadLocalMap的set策略:
遍历所有Entry, 找到对应存在的则不set, 直接返回。如果未找到, 发现Entry有不存在的key, 则调用replaceStaleEntry替换之。遍历完一遍后无果, 在new一个新的Entry

关于cleanSomeSlots是ThreadLocal的内存回收过程, 后文会详细讲解。

以上就是ThreadLocal, get的调用过程,其set方法的调用过程与get方法非常类似,这里就不重复讲解了。

4.ThreadLocal的内存回收

在说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方法中有下面的代码:

  1. if (!cleanSomeSlots(i, sz) && sz >= threshold)
  2. rehash();

下面看一下cleanSomeSlots源码:

  1. private boolean cleanSomeSlots(int i, int n) {
  2. boolean removed = false;
  3. Entry[] tab = table;
  4. int len = tab.length;
  5. do {
  6. i = nextIndex(i, len);
  7. Entry e = tab[i];
  8. if (e != null && e.get() == null) {
  9. n = len;
  10. removed = true;
  11. i = expungeStaleEntry(i);
  12. }
  13. } while ( (n >>>= 1) != 0);
  14. return removed;
  15. }

cleanSomeSlots方法从源码上看就是找出废弃的Entry并清除。其中查找Entry的过程却执行了log2(n)次。这样做的目的是提高效率。

下面是expungeStaleEntry(int i)方法源码:

  1. private int expungeStaleEntry(int staleSlot) {
  2. Entry[] tab = table;
  3. int len = tab.length;
  4. // expunge entry at staleSlot
  5. tab[staleSlot].value = null;
  6. tab[staleSlot] = null;
  7. size--;
  8. // Rehash until we encounter null
  9. Entry e;
  10. int i;
  11. for (i = nextIndex(staleSlot, len);
  12. (e = tab[i]) != null;
  13. i = nextIndex(i, len)) {
  14. ThreadLocal k = e.get();
  15. if (k == null) {
  16. e.value = null;
  17. tab[i] = null;
  18. size--;
  19. } else {
  20. int h = k.threadLocalHashCode & (len - 1);
  21. if (h != i) {
  22. tab[i] = null;
  23. // Unlike Knuth 6.4 Algorithm R, we must scan until
  24. // null because multiple entries could have been stale.
  25. while (tab[h] != null)
  26. h = nextIndex(h, len);
  27. tab[h] = e;
  28. }
  29. }
  30. }
  31. return i;
  32. }

expungeStaleEntry的作用清除无用Entry,以及解决在无用Entry之间与下一个可用空间之间的hash冲突问题

至于rehash()方法源代码中的注释也说得很明白, 即先清除一遍无用Entry, 再扩容。

  1. /**
  2. * Re-pack and/or re-size the table. First scan the entire
  3. * table removing stale entries. If this doesn't sufficiently
  4. * shrink the size of the table, double the table size.
  5. */
  6. private void rehash() {
  7. expungeStaleEntries();
  8. // Use lower threshold for doubling to avoid hysteresis
  9. if (size >= threshold - threshold / 4)
  10. resize();
  11. }

参考文献:深入JDK源码之ThreadLocal类

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注