Java-多线程-ThreadLocal全解析

Java-多线程-ThreadLocal全解析

1 摘要

本文简单分析下ThreadLocal实现原理,再附上小例子。

2 ThreadLocal是什么

ThreadLocal提供线程级别的私有局部变量。这些变量和普通变量不同之处在于,通过get或set方法访问这类变量的每个线程都拥有一份独立初始化的变量副本

ThreadLocal通常用private static修饰,可以将状态与该线程建立一对一的关系。

下面这个小例子,当第一次调用ThreadId.get()时会为每个线程生成一个全局唯一的、以1为步长自增的标识符,往后都会保持不变。

import java.util.concurrent.atomic.AtomicInteger;

 public class ThreadId {
     // Atomic integer containing the next thread ID to be assigned
     private static final AtomicInteger nextId = new AtomicInteger(0);

     // Thread local variable containing each thread's ID
     private static final ThreadLocal<Integer> threadId =
         new ThreadLocal<Integer>() {
             @Override protected Integer initialValue() {
                 return nextId.getAndIncrement();
         }
     };

     // Returns the current thread's unique ID, assigning it if necessary
     public static int get() {
         return threadId.get();
     }
 }

在上面的例子中,只要线程存活且该ThreadLocal实例可访问,那么每个线程都会拥有一个隐式引用,指向自己拥有的ThreadLocal变量副本值。

  • 下图上半部分展示两个线程共享一个普通静态变量的情况,下半部分展示ThreadLocal持有的变量情况:
    Java-多线程-ThreadLocal全解析_第1张图片

3 原理

Java-多线程-ThreadLocal全解析_第2张图片
总的来说,其实就是多个线程共用一个某个类的静态ThreadLocal Instance,由线程级别的Map中的Entry的弱引用来指向这个共同的ThreadLocal Instance,而这个Entry的Value就是存的用户自定义的Value,所以可以达到线程独有一份的目的。关于Java内的各种引用可参考Java-内存模型-引用总结

ThreadLoca原理要点如下:

  • 每个线程有自己的一个全局ThreadLocalMap实例,ThreadLocalMap的key为ThreadLocal对象,value为用户自定义值。也就是说这个map可以放多个ThreadLocal对象。

  • 但是一般来说,每次使用时,每个ThreadLocal实例被多个线程共享而不是每个线程一个ThreadLocal实例。而且,我们应用的类对象里面有一个强引用指向ThreadLocal实例。

    比如以下代码,就是多个TestThread线程实例共享一个ThreadLocal Pet对象,但每个线程拥有一份独占的受ThreadLocal保护的Dog实例:

    public class Demo2
    {
    	private static ThreadLocal<Dog> pet = new ThreadLocal<Dog>(){
    	    @Override
    	    protected Dog initialValue()
    	    {
    	        return new Dog("tom", 1);
    	    }
    	};
    	private static class TestThread implements Runnable{
        @Override
        public void run()
        {
        	// 获取由ThreadLocal保护的线程级别的初始值
            Dog dog = pt.get();
            // 改变该值
            pt.set(new Dog("newName", 19));
            // 从线程的ThreadLocalMap中清理该ThreadLocal对象
            pt.remove();
        }
    }
    
  • ThreadLocalMap有一个Entry数组,内嵌的Entry类其实是继承自WeakReference>,即弱引用。这个弱引用指向该ThreadLocal实例(注意不是key去指向,因为这个Entry里根本就没有key这样一个对象)。

  • ThreadLocalMap的Entry构造方法是Entry(ThreadLocal k, Object v) { super(k); value = v; }

    其中value是用户自定义的存储的值。

  • ThreadLocalMap的数据结构只有个Entry数组,而且放入元素发生Hash冲突时不是放入这个位置的链表而是调用nextIndex方式继续查找适合的位置,在此过程中会调用一些方法清理失效(指Entry指向ThreadLocal实例的弱引用已经被GC清理)的Entry对,如果还是空间不够就扩容。

注意:网上很多文章说ThreadLocal原理是有个Map以每个Thread实例为key,这是绝对错误的。
还有人说Entry的key有一个弱引用指向ThreadLocal实例,也是错误的,因为Entry对象本身就是一个指向ThreadLocal实例的弱引用,存放在ThreadLocalMap的Entry数组内。

3.1 ThreadLocal的属性

3.1.1 HashCode

private final int threadLocalHashCode = nextHashCode();

// 两个连续的hashcode差值
private static final int HASH_INCREMENT = 0x61c88647;

private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
}

private static AtomicInteger nextHashCode = new AtomicInteger();

ThreadLocalMap类型的Thread.threadLocals和inheritableThreadLocals依赖于附属于每个线程的线性探测哈希映射。

ThreadLocal对象充当key,通过threadLocalHashCode搜索。 这是一个自定义哈希代码(仅在ThreadLocalMaps中有用),它消除了在相同线程使用连续构造的ThreadLocals的常见情况下的冲突,同时在不太常见的情况下保持良好行为。

3.1.2 HASH_INCREMENT = 0x61c88647

关于HASH_INCREMENT = 0x61c88647 这个魔数,网上查了一些资料,大致原因是因为此数为黄金分割,可以让数组内的hash分布更均匀。更多内容可以参考:What is the meaning of 0x61C88647 constant in ThreadLocal.java

3.2 ThreadLocal构造方法

 public ThreadLocal() {}

这里什么都没做。

3.3 成员方法

3.3.1 ThreadLocal.initialValue

protected T initialValue() {
        return null;
}

这个方法会在第一次使用get方法时调用setInitialValue时执行,除非在此之前已经调用了set方法。

一般来说该方法只会被调用一次,但如果使用remove清空随后又调用get方法时,又会再次调用initialValue

可以看到该方法默认返回的初值为null,如果我们想自定义一个初值,一般就是用匿名内部类的方式重写该方法实现自定义初值,比如以下代码定义了返回一个new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")DateFormat类型的初值的ThreadLocal:

ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
        @Override
        protected DateFormat initialValue() {
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
} };

3.3.2 ThreadLocal.get

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
}

get方法很重要,他被用来获取当前thread私有的threadlocal保护的变量副本。

如果还没有当前线程的ThreadLocalMap或者该map中不存在以该ThreadLocal对象为key的Entry,那此时该线程的ThreadLocal保护的变量副本肯定不存在,则此时首先将其初始化为调用initialValue方法的返回值。

ThreadLocal.setInitialValue方法如下:

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;
}

可以看到他会先调用initialValue方法去拿到初始值,然后获取当前线程的ThreadLocalMap。如果map不存在就以当前threadvalue创建ThreadLocalMap;如果已经存在就以当前ThreadLocal实例为key,value为值放入该ThreadLocalMap。

ThreadLocal.getMap方法如下:

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

注意,这里的t就是当前线程对象,t.threadLocals在Thread类里的声明如下:

ThreadLocal.ThreadLocalMap threadLocals = null;

可以看到,threadLocals其实就是每个线程对象独有的一个ThreadLocalMap类型的实例对象。具体可参考后文ThreadLocalMap

3.3.3 ThreadLocal.set

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
}

该方法用来设置ThreadLocal保护的值,跟前面提到的setInitialValue方法差不多,只不过这里指定了放入ThreadLocalMap的自定义value。

3.3.4 ThreadLocal.remove

 public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
}

该方法用于获取当前线程的ThreadLocalMap,然后从其中移除当前ThreadLocal实例为key的Entry

3.4 内部类-SuppliedThreadLocal

 static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {

        private final Supplier<? extends T> supplier;

        SuppliedThreadLocal(Supplier<? extends T> supplier) {
            this.supplier = Objects.requireNonNull(supplier);
        }

        @Override
        protected T initialValue() {
            return supplier.get();
        }
}

这个类的主要作用是配合withInitial设定初始值,其实就是利用Supplier的lambda表达式写法。示例如下:

private static ThreadLocal<Dog> pt = ThreadLocal.withInitial(() -> new Dog("tom", 1));

3.5 ThreadLocalMap

3.5.1 简述

Java-多线程-ThreadLocal全解析_第3张图片
ThreadLocalMap是一个自定义的类HashMap,他只适合维护threadlocal values,没有向外暴露任何方法。

为了应对长期和高负荷的使用,所以采用了WeakReference来修饰该map的key。也就是说当这些key无其他强引用时,GC会将他们回收。但需要注意的是,因为创建弱引用key的时候没有采用RefereceQueue,所以只能保证那些已经没用的的Entry会在entry table超出大小限制时被移除。

3.5.2 ThreadLocalMap类声明

static class ThreadLocalMap 

可以看到,ThreadLocalMap是ThreadLocal里的一个静态内部类,而且限制使用域是本类、和同包(java.lang,包括Thread类)。

3.5.3 成员变量

以下是ThreadLocalMap的一些成员标量,和HashMap类似:

// Entry Table初始容量
private static final int INITIAL_CAPACITY = 16;

// Entry数组,按需库容,且长度必须是2的n次方
private Entry[] table;

// Entry数组元素个数
private int size = 0;

// Entry数组扩容时的阈值,默认为0
private int threshold;

3.5.4 内部类Entry

static class Entry extends WeakReference<ThreadLocal<?>> {
    // 存放指定的value
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
    	// 调用父类WeakReference构造方法,构建一个指向ThreadLocal实例的弱引用
        super(k);
        // 保存指定的value值
        value = v;
    }
}

Entry是ThreadLocalMap的静态内部类,他继承自WeakReference,也就是说Entry本身是个指向ThreadLocal对象的弱引用,记住这一点十分重要

可以看到Entry内部只显示存放了指定的value对象,而构造方法Entry(ThreadLocal k, Object v)里构建了一个到ThreadLocal实例的弱引用。

调用Entry.get方法其实就是调用其祖先类Reference.get方法,获取到的referent值为null时,说明已经该弱引用已经不存在了,代表此时该Entry可以被Entry数组剔除,这种无效的Entry被称为stale entry即过期的Entry。

3.5.5 ThreadLocalMap构造方法

有两个:一个是public一个是private。

  • pubic
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    	 // 初始长度为16的Entry数组
         table = new Entry[INITIAL_CAPACITY];
         // 用每个key的threadLocalHashCode和(1111)按位做与操作得到Entry应该放的下标
         // 这样做的好处就是不管你threadLocalHashCode再大,计算结果也不会超过15
         int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
         // 初始化Entry,并放入数组对应位置
         table[i] = new Entry(firstKey, firstValue);
         // Entry数组大小更改为1
         size = 1;
         // 根据容量重设扩容阈值
         setThreshold(INITIAL_CAPACITY);
     }
    
    这个方法就是传入第一个ThreadLocal对象作为key,value作为值,构建一个ThreadLocalMap。需要注意的是,ThreadLocalMap是懒创建的,也就是说直到有Entry需要加入才会调用此方法。可以参考之前ThreadLocal.setInitialValueThreadLocal.set方法,里面有调用ThreadLocal.createMap方法:
    void createMap(Thread t, T firstValue) {
    	// 这里就以本ThreadLocal对象为key,首个自定义值为value来构造ThreadLocalMap
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    
  • private
    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) {
                @SuppressWarnings("unchecked")
                ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                if (key != null) {
                    Object value = key.childValue(e.value);
                    Entry c = new Entry(key, value);
                    int h = key.threadLocalHashCode & (len - 1);
                    while (table[h] != null)
                        h = nextIndex(h, len);
                    table[h] = c;
                    size++;
                }
            }
        }
    }
    
    这个构造方法由createInheritedMap方法调用,传入的参数是父线程的ThreadLocalMap。将会创建一个ThreadLocalMap包括所有父map内的ThreadLocal。

3.5.6 扩容相关方法

// 设置扩容阈值为容量的三分之二
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

// 下标i+1的方式从小往大的下标方向递推
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

// 下标i-1的方式从大往小的下标方向递推
private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}

3.5.7 扩容方法resize

将Entry数组扩容为以前大小的两倍,顺便清理发生hash碰撞时key已为null的entry,具体步骤如下:

  1. 创建新的Entry[]
  2. 然后循环遍历老的Entry[]
  3. 如果Entry元素存在就去获取弱引用的ThreadLocal实例,
  4. 通过Reference.get方法判断ThreadLocal引用是否为空
    1. 此时如果ThreadLocal引用为空,代表由于Entry为弱引用导致该引用已经被GC回收,那么直接把当前Entry置为null做清理;
    2. ThreadLocal引用不为空情况的处理:
      1. 否则通过threadLocalHashCode和 (新的数组长度-1) 按位与的方式得到新的数组下标
      2. 接着判断该下标位置是否已存在元素,若存在就反复调用nextIndex方法从小往大的下标方向递推求新的下标
      3. 找到合适位置后,将元素entry放入新的数组中的下标位置,并将临时元素计数器加一
  5. 更新扩容阈值,并将临时变量赋值给对应的entry table实例变量。扩容完成。

代码如下:

private void resize() {
	// 1.创建新的Entry[],长度为旧数组两倍
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;
	// 2.然后循环遍历老的Entry[]
    for (int j = 0; j < oldLen; ++j) {
        Entry e = oldTab[j];
        if (e != null) {
        	// 3.如果Entry元素存在就去获取弱引用的ThreadLocal
            ThreadLocal<?> k = e.get();
            if (k == null) {
            	// 4.1此时如果ThreadLocal引用已经为空,代表已经被GC回收,那么直接把当前Entry置为null;
                e.value = null; // Help the GC
            } else {
            	// 4.2.1否则通过threadLocalHashCode和(新的数组长度-1)按位与的方式得到新的数组下标
                int h = k.threadLocalHashCode & (newLen - 1);
                // 4.2.2接着判断该下标位置是否已存在元素,若存在就反复调用nextIndex方法求新的下标
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                // 4.2.3将元素entry放入新的数组中的下标位置,并将临时元素计数器加一
                newTab[h] = e;
                count++;
            }
        }
    }
	// 5.更新扩容阈值,并将临时变量赋值给对应的entry table实例变量。扩容完成。
    setThreshold(newLen);
    size = count;
    table = newTab;
}

3.5.8 放入元素set

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.
	
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
	// 寻找新entry放入的适合位置。
	// hash冲突时再hash方式为nextIndex从下标小的方式往大的方向递推
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
		// 如果计算出的下标位置存在entry
		// 且该ThreadLocal实例key和参数ThreadLocal对象相同,那就更新value。set结束
        if (k == key) {
            e.value = value;
            return;
        }
		// 如果计算出的下标位置存在entry且该ThreadLocal实例key为null
		// 此时说明该entry的弱引用已经失效,就用生成新的entry替换。set结束
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
        // 如果计算出的下标位置存在entry且该ThreadLocal实例key和参数ThreadLocal不同,就进入下一次循环
        // 否则说明该位置e = null ,跳出循环
    }
	// 此时e = null,也就是说该数组位置不存在entry
	// 用参数生成一个新的entry,放入此位置即可
    tab[i] = new Entry(key, value);
    // 大小加一
    int sz = ++size;
    // 如果没有发生清理行为且 当前数组元素个数达到扩容阈值,就需要rehash
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

ThreadLocalMap put元素遇到有效Entry对,发生hash冲突时,不同于hashMap是放入该位置的链表,而是通过nextIndex方法从下标小的往大的方向递推继续找合适位置。

3.5.8 获取元素

// 获取ThreadLocal实例对应的Entry
// 如果没能匹配到就调用getEntryAfterMiss
private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

// 当没有找到ThreadLocal实例对应的Entry时,只能调用该方法来查找
// 这个方法也会在查找过程中顺便清理无效的弱引用Entry
// i为数组下标,e为当前下标对应的Entry
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
        	// 找到该entry就返回
            return e;
        if (k == null)
        	// 该位置ThreadLocal引用为空,清理该etnry
            expungeStaleEntry(i);
        else
        	// 否则继续递推查找
            i = nextIndex(i, len);
        e = tab[i];
    }
    // 到这里,说明没有找到
    return null;
}

可以看到在没有一次性找到对应位置的元素的时候调用了getEntryAfterMiss方法,会在查找过程中不断清理无效的弱引用Entry。

3.5.9 移除元素

// 将传入的参数ThreadLocal实例对应的Entry弱引用去掉,并把Entry从map中移除
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;
        }
    }
}

3.5.10 替换失效Entry

// 替换失效的数组位置上的Entry,在此过程中将遇到的失效弱引用Entry移除
// key和value为新的值,staleSlot为匹配到的第一个失效Entry下标
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
   Entry[] tab = table;
   int len = tab.length;
   Entry e;

   // We clean out whole runs at a time to avoid continual
   // incremental rehashing due to garbage collector freeing
   // up refs in bunches (i.e., whenever the collector runs).
   // 倒推以检查当前运行中的已失效Entry。 
   // 我们一次清理整个运行,以避免由于垃圾收集器释放串联的refs(即,每当收集器运行时)不断的增量重复。
   int slotToExpunge = staleSlot;
   // 这里得到的slotToExpunge是从后往前推的最后一个e!=null但是e.get()==null的下标
   for (int i = prevIndex(staleSlot, len);
        (e = tab[i]) != null;
        i = prevIndex(i, len))
       if (e.get() == null)
           slotToExpunge = i;

   // 找到的key或尾部的空元素
   for (int i = nextIndex(staleSlot, len);
        (e = tab[i]) != null;
        i = nextIndex(i, len)) {
       ThreadLocal<?> k = e.get();

       // If we find key, then we need to swap it
       // with the stale entry to maintain hash table order.
       // The newly stale slot, or any other stale slot
       // encountered above it, can then be sent to expungeStaleEntry
       // to remove or rehash all of the other entries in run.
       // 如果匹配到键,那么我们需要将它与失效Entry交换以维护哈希表顺序。 
       // 然后可以将新陈旧的插槽或其上方遇到的任何其他陈旧插槽发送到expungeStaleEntry以删除或重新运行运行中的所有其他条目。
       if (k == key) {
       	   // 如果存在该ThreadLocal实例的Entry,就覆盖该value
           e.value = value;
           // 这里staleSlot为匹配到的第一个失效Entry下标
		   // 赋值后tab[i]!=null但是tab[i].get()==null
           tab[i] = tab[staleSlot];
           // 原staleSlot Entry替换为value更新后的e
           tab[staleSlot] = e;

           // Start expunge at preceding stale entry if it exists
           // slotToExpunge == staleSlot的情况是在前面倒推运算中没有找到失效的Entry
           if (slotToExpunge == staleSlot)
           	   // 注意这里i下标对应的元素是tab[staleSlot]
               slotToExpunge = i;
           // 移除slotToExpunge位置的Entry顺便移除一些失效Entry
           cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
           return;
       }

       // If we didn't find stale entry on backward scan, the
       // first stale entry seen while scanning for key is the
       // first still present in the run.
       // 该Entry弱引用失效,且在前面倒推查找中没有匹配到失效的Entry
       if (k == null && slotToExpunge == staleSlot)
       		// 那就把当前这个对应失效弱引用的i给slotToExpunge,然后继续循环
           slotToExpunge = i;
   }

   // If key not found, put new entry in stale slot
   // 走到这里说明没有匹配到ThreadLocal key
   // 把staleSlot处的value设为空(help gc)
   tab[staleSlot].value = null;
   // 该位置设为由新的ThreadLocal实例为key,新value的Entry
   tab[staleSlot] = new Entry(key, value);

   // 不相等说明找到了另外的失效的Entry位置,干掉他们并顺便清理出一点空间
   if (slotToExpunge != staleSlot)
       cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

3.5.11 自动过期

// 清理一些Entry
// 清理次数由当前数组大小是2的多少倍决定
// 所以叫cleanSomeSlots - -|
// 当发生了清理就返回true,否则返回false
private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    // 找到下一个下标位置,如果失效entry就干掉
    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;
}


// 先清理所有失效的Entry
// 如果 此时size还是大于之前的四分之三,就扩容
private void rehash() {
    expungeStaleEntries();

    // Use lower threshold for doubling to avoid hysteresis
    if (size >= threshold - threshold / 4)
        resize();
}

// 遍历数组,移除失去弱引用的旧Entry
private void expungeStaleEntries() {
     Entry[] tab = table;
     int len = tab.length;
     for (int j = 0; j < len; j++) {
         Entry e = tab[j];
         // 找到失去弱引用的Entry,将该Entry所在下标传入expungeStaleEntry将其干掉
         if (e != null && e.get() == null)
             expungeStaleEntry(j);
     }
}

// 此方法是真正移除失去弱引用的Entry的方法,顺便移除遇到的碰撞位置的失效Entry
// 参数staleSlot 是该Entry所在下标
// 返回下标i,此时tab[i]为null
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    // 数组原始长度
    int len = tab.length;

    // 移除Entry的value和本身的引用
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    // 数组大小减一
    size--;

    // Rehash until we encounter null
    Entry e;
    int i;
    // 循环的方式去掉弱引用失效的entry
    // 循环开始条件是让该下标对原始数组长度求模
    // 循环结束条件是Entry为null
    // 每次循环完又再次nextIndex计算新下标
    // 循环过程中还会把清理了entry位置的按nextIndex的往后查找
    // 将找到的弱引用为null的清理,
    // 非null且为递推移动放入的元素按规则放入新位置
    // 这样可以避免清理元素后无法递推查找其他元素
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {
        	// 找到弱引用失效的entry,干掉
            e.value = null;
            tab[i] = null;
            size--;
        } else {
        	// 此时表示e的弱引用存在
        	
        	// 我们已经熟悉了这种获取下标方式
            int h = k.threadLocalHashCode & (len - 1);
            // 不相等表示存在hash冲突,放入的位置是用N次nextIndex计算后的新位置
            // 现在由于递推之前的位置发生了清理,所以会导致递推查找失败
            // 现在必须把该位置的entry放到新的null位置
            if (h != i) {
            	// 将递推i位置的Entry置空
                tab[i] = null;

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                // 找到一个新的无entry的数组下标
                while (tab[h] != null)
                    h = nextIndex(h, len);
                // 将e移动到新的数组下标位置
                tab[h] = e;
            }
        }
    }
    // 此时tab[i]为空
    return i;
}

4 坑

4.1 内存泄露

因为代码中一般长期存在指向ThreadLocal对象的强引用,那么,使用了该ThreadLocal的线程的ThreadLocalMap里的Entry之key,虽是指向该ThreadLocal对象的弱引用,但是因为代码对ThreadLocal强引用(就是声明如private static ThreadLocal pt = new ThreadLocal()的强引用)持续存在,导致这类弱引用Entry无法被及时回收。

也就是说,这类弱引用Entry会随着线程持续存在而存在,造成内存泄露。所以我们应该在每个线程使用完ThreadLocal对象后,调用remove方法,手动移除该Entry。

4.2 线程池

线程池中的Thread对象就那么几个,都是复用的。也就是说,他们的ThreadLocalMap对象是不会变的,会导致Runnable运行时的ThreadLocal值交叉混用,出现问题。那么就需要在每个Runnable的run方法执行完后执行threadLocal.remove(),示例如下:

 ExecutorService executorService = new ThreadPoolExecutor(5,5,1, TimeUnit.MINUTES,workQueue,new ThreadPoolExecutor.DiscardPolicy()){
            @Override
            protected void afterExecute(Runnable r, Throwable t)
            {
                threadLocalName.remove();

                super.afterExecute(r, t);
            }
};

5 例子

5.1 DateFormat

SimpleDateFormat并不是线程安全的,所以在阿里的Java开发规范里推荐了用ThreadLocal保证线程安全的做法:

private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
        @Override
        protected DateFormat initialValue() {
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    } };

  String startTimeStr = "2018-01-11 02:17:02.806";
        try {
            Date startTime = df.get().parse(startTimeStr);
            System.out.println("startTime=" + startTime);
            String dfStr = df.get().format(new Date());
            System.out.println("dfStr=" + dfStr);
		}catch (ParseException e) {
            e.printStackTrace();
        }

参考文档

JavaDoc-java.lang.ThreadLocal

彻底理解ThreadLocal

ThreadLocal原理及内存泄露预防

图解Java多线程

你可能感兴趣的:(java,多线程)