ThreadLocal 解析

简单使用

//咱们只须要实例化一次,做为key就够了,或者使用静态。
private  ThreadLocal myThreadLoca<String>l = new ThreadLocal();

// 写入
myThreadLocal.set("A thread local value");

// 读取
String threadLocalValue = myThreadLocal.get();
复制代码

ThreadLocal 与Thread 同步机制的比较

同步机制 -- “以时间换空间”

在同步机制中,经过对象的锁机制保证同一时间只有一个线程访问变量,实现串行化.这时该变量是多个线程共享的,使用同步机制要求程序缜密地分析何时对变量进行读/写、何时须要锁定某个对象、何时释放对象锁等繁杂的问题,程序设计和编写难度相对较大。java

ThreadLocal -- “以空间换时间”

给每一个线程创建副本,互不干扰.数组

总结

对于多线程资源共享的问题.安全

同步机制采用“以时间换空间”的方式:访问串行化,对象共享化 ; ThreadLocaI采用了“以空间换时间”的方式:访问并行化,对象独享化。数据结构

前者仅提供一份变量,让不一样的线程排队访问;然后者为每一个线程都提供了一份变量,所以能够同时访问而互不影响。多线程

ThreadLocal 源码分析

ThreadLocal的Hash值的计算

// hash code
private final int threadLocalHashCode = nextHashCode();
 
// AtomicInteger类型,从0开始
private static AtomicInteger nextHashCode =
    new AtomicInteger();
 
// hash code每次增长1640531527 
private static final int HASH_INCREMENT = 0x61c88647;
 
// 下一个hash code
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}
复制代码

ThreadLocal的hashcode(threadLocalHashCode)是从0开始,每新建一个ThreadLocal,对应的hashcode就加0x61c88647.工具

ThreadLocal的set方法

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t); // 获取当前线程的ThreadLocalMap
    // 当前线程的ThreadLocalMap不为空则调用set方法, this为调用该方法的ThreadLocal对象
    if (map != null) 
        map.set(this, value);
    // map为空则调用createMap方法建立一个新的ThreadLocalMap, 并新建一个Entry放入该
    // ThreadLocalMap, 调用set方法的ThreadLocal和传入的value做为该Entry的key和value
    else
        createMap(t, value);    
}
 
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;	// 返回线程t的threadLocals属性,类型是ThreadLocalMap
}
复制代码
  1. 先拿到当前线程,再使用getMap方法拿到当前线程的threadLocals变量(类型ThreadLocalMap)
  2. 若是threadLocals不为空,则将当前ThreadLocal做为key,传入的值做为value,调用set方法(见下文ThreadLocalMap的set方法)插入threadLocals。
  3. 若是threadLocals为空则调用建立一个ThreadLocalMap,并新建一个Entry放入该ThreadLocalMap, 调用set方法的ThreadLocal自身和传入的value做为该Entry的key和value

注意此处的threadLocals变量是一个ThreadLocalMap,是Thread的一个局部变量,所以它只与当前线程绑定。源码分析

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();
    }
复制代码

也是先得到当前线程的ThreadLocalMap变量.其实ThreadLocal类就像是一个工具类同样,核心是其内部类ThreadLocalMap.this

ThreadLocalMap 源码分析

示意图

ThreadLocalMap是一个自定义哈希映射,仅用于维护线程本地变量值。
ThreadLocalMap是ThreadLocal的内部类,主要有一个Entry数组,Entry的key为ThreadLocal,value为ThreadLocal对应的值。
每一个线程都有一个ThreadLocalMap类型的threadLocals变量。spa

成员变量和相关方法

// 初始容量16
private static final int INITIAL_CAPACITY = 16;
// entry数组
private Entry[] table;
// entry 元素个数
private int size = 0;
// 扩容阀值
private int threshold; // Default to 0
// 一次set是2/3 len
private void setThreshold(int len) {  threshold = len * 2 / 3;  }

//下面两个方法能够向前/后获取坐标.
private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0);}

private static int prevIndex(int i, int len) {return ((i - 1 >= 0) ? i - 1 : len - 1); }
				
复制代码

Entry实体

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
 
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}
复制代码

结合示意看,entry中的key是一个弱引用.它的存在不会使得ThreadLocal变量豁免垃圾回收.当定义ThreadLocal变量的线程终止,没有强引用关联ThreadLocal变量,它就会被垃圾回收..net

说到底ThreadLocal变量只是一个key而已,能够用它去各个线程本身的ThreadLocalMap中查询,它自己并不存储信息.

固然这会形成各个线程的ThreadLocalMap中出现key为null的entry.这个在后边内存泄漏会讲到.

扩容

private void rehash() {
    expungeStaleEntries();  // 调用expungeStaleEntries方法清理key为null的Entry
 
    // 若是清理后size超过阈值的3/4, 则进行扩容
    if (size >= threshold - threshold / 4)  
		    // 大小*2 
				// h = k.threadLocalHashCode & (newLen - 1);
        resize();
}
复制代码

清理key为null的entry

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) {	// 遍历到key为null的元素
            n = len;	// 重置n的值
            removed = true;	// 标志有移除元素
            i = expungeStaleEntry(i);	// 移除i位置及以后的key为null的元素
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}
复制代码

从 i 开始,清除key为空的Entry,扫描次数是log2n,当遍历到一个key为null的元素时,调用expungeStaleEntry清除,并将遍历次数重置。 n的值多是当前元素个数size(从增长元素操做调过来).或者整个entry长度len(从replaceStaleEntry方法调过来). 官方给出的解释是这个方法简单、快速,而且效果不错。

expungeStaleEntry 方法

// 从staleSlot开始, 清除key为空的Entry, 并将不为空的元素放到合适的位置,最后返回Entry为空的位置
private int expungeStaleEntry(int staleSlot) {  
    Entry[] tab = table;
    int len = tab.length;
 
    // expunge entry at staleSlot
    tab[staleSlot].value = null;    // 将tab上staleSlot位置的对象清空
    tab[staleSlot] = null;
    size--;
 
    // Rehash until we encounter null
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len); // 遍历下一个元素, 即(i+1)%len位置的元素
         (e = tab[i]) != null;	// 遍历到Entry为空时, 跳出循环并返回索引位置
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get(); 
        if (k == null) {    // 当前遍历Entry的key为空, 则将该位置的对象清空
            e.value = null;
            tab[i] = null;
            size--;
        } else {    // 当前遍历Entry的key不为空
            int h = k.threadLocalHashCode & (len - 1);  // 从新计算该Entry的索引位置
            if (h != i) {   // 若是索引位置不为当前索引位置i
                tab[i] = null;  // 则将i位置对象清空, 替当前Entry寻找正确的位置
 
                // 若是h位置不为null,则向后寻找当前Entry的位置
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;   
}
复制代码

ThreadLocalMap的set方法

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);  // 计算出索引的位置
 
    // 从索引位置开始遍历,因为不是链表结构,所以经过nextIndex方法来寻找下一个索引位置 
    for (Entry e = tab[i];
         e != null;	// 当遍历到的Entry为空时结束遍历
         e = tab[i = nextIndex(i, len)]) {  
        ThreadLocal<?> k = e.get(); // 拿到Entry的key,也就是ThreadLocal 
 
        // 该Entry的key和传入的key相等, 则用传入的value替换掉原来的value 
        if (k == key) {
            e.value = value;
            return;
        }
 
        // 该Entry的key为空, 则表明该Entry须要被清空, 
        // 调用replaceStaleEntry方法 
        if (k == null) {
        	// 该方法会继续寻找传入key的安放位置, 并清理掉key为空的Entry 
            replaceStaleEntry(key, value, i);
            return;
        }
    }
 
    // 寻找到一个空位置,&emsp;则放置在该位置上
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // cleanSomeSlots是用来清理掉key为空的Entry,若是此方法返回true,则表明至少清理
    // 了1个元素, 则这次set必然不须要扩容, 若是此方法返回false则判断sz是否大于阈值
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();   // 扩容
}
复制代码
  1. 经过传入的key的hashCode计算出索引的位置
  2. 从索引位置开始遍历,因为不是链表结构,所以经过nextIndex方法来寻找下一个索引位置
  3. 若是找到某个Entry的key和传入的key相同,则用传入的value替换掉该Entry的value。
  4. 若是遍历到某个Entry的key为空,则调用replaceStaleEntry方法
  5. 若是经过nextIndex寻找到一个空位置(表明没有找到key相同的),则将元素放在该位置上
  6. 调用cleanSomeSlots方法清理key为null的Entry,并判断是否须要扩容,若是须要则调用rehash方法进行扩容。

replaceStaleEntry 方法

private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;
 
    int slotToExpunge = staleSlot;	// 清除元素的开始位置(记录索引位置最前面的)
    // 向前遍历,直到遇到Entry为空
    for (int i = prevIndex(staleSlot, len); 
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;	// 记录最后一个key为null的索引位置
 
    // Find either the key or trailing null slot of run, whichever
    // occurs first
    for (int i = nextIndex(staleSlot, len); // 向后遍历,直到遇到Entry为空
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
 
        // 该Entry的key和传入的key相等, 则将传入的value替换掉该Entry的value
        if (k == key) {
            e.value = value;
 
            // 将i位置和staleSlot位置的元素对换(staleSlot位置较前,是要清除的元素)
            tab[i] = tab[staleSlot];	
            tab[staleSlot] = e;
 
            // 若是相等, 则表明上面的向前寻找key为null的遍历没有找到,
            // 即staleSlot位置前面的元素没有须要清除的,此时将slotToExpunge设置为i, 
            // 由于原staleSlot的元素已经被放到i位置了,这时位置i前面的元素都不须要清除
            if (slotToExpunge == staleSlot) 
                slotToExpunge = i;
            // 从slotToExpunge位置开始清除key为空的Entry
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }
 
        // 若是第一次遍历到key为null的元素,而且上面的向前寻找key为null的遍历没有找到,
        // 则将slotToExpunge设置为当前的位置
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }
 
    // 若是key没有找到,则新建一个Entry,放在staleSlot位置
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);
 
    // 若是slotToExpunge!=staleSlot,表明除了staleSlot位置还有其余位置的元素须要清除
    // 须要清除的定义:key为null的Entry,调用cleanSomeSlots方法清除key为null的Entry
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
复制代码
  1. slotToExpunge始终记录着须要清除的元素的最前面的位置(即slotToExpunge前面的元素是不须要清除的)
  2. 从位置staleSlot向前遍历,直到遇到Entry为空,用staleSlot记录最后一个key为null的索引位置(也就是遍历过位置最前的key为null的位置)
  3. 从位置staleSlot向后遍历,直到遇到Entry为空,若是遍历到key和入参key相同的,则将入参的value替换掉该Entry的value,并将i位置和staleSlot位置的元素对换(staleSlot位置较前,是要清除的元素),遍历的时候判断slotToExpunge的值是否须要调整,最后调用expungeStaleEntry方法和cleanSomeSlots方法清除key为null的元素。
  4. 若是key没有找到,则使用入参的key和value新建一个Entry,放在staleSlot位置
  5. 判断是否还有其余位置的元素key为null,若是有则调用expungeStaleEntry方法和cleanSomeSlots方法清除key为null的元素

源码总结

  1. 每一个线程都有一个ThreadLocalMap 类型的 threadLocals 属性。
  2. ThreadLocalMap 类至关于一个Map,key 是 ThreadLocal 自己,value 就是咱们的值。
  3. 当咱们经过 threadLocal.set(new Integer(123)); ,咱们就会在这个线程中的 threadLocals 属性中放入一个键值对,key 是 这个 threadLocal.set(new Integer(123))的threadlocal,value 就是值new Integer(123)。
  4. 当咱们经过 threadlocal.get() 方法的时候,首先会根据这个线程获得这个线程的 threadLocals 属性,而后因为这个属性放的是键值对,咱们就能够根据键 threadlocal 拿到值。 注意,这时候这个键 threadlocal 和 咱们 set 方法的时候的那个键 threadlocal 是同样的,因此咱们可以拿到相同的值。
  5. ThreadLocalMap 的get/set/remove方法跟HashMap的内部实现都基本同样,经过 "key.threadLocalHashCode & (table.length - 1)" 运算式计算获得咱们想要找的索引位置,若是该索引位置的键值对不是咱们要找的,则经过nextIndex方法计算下一个索引位置,直到找到目标键值对或者为空。
  6. hash冲突:在HashMap中相同索引位置的元素以链表形式保存在同一个索引位置;而在ThreadLocalMap中,没有使用链表的数据结构,而是将(当前的索引位置+1)对length取模的结果做为相同索引元素的位置: 源码中的nextIndex方法,能够表达成以下公式:若是i为当前索引位置,则下一个索引位置 = (i + 1 < len) ? i + 1 : 0

问题辨析

为何ThreadLocal一般被static修饰?

ThreadLocal 是一个key,去各个线程本身的map里取值,不必多个.

Java 中每一个线程都有与之关联的Thread对象,Thread对象中有一个ThreadLocal.ThreadLocalMap类型的成员变量,该变量是一个Hash表.因此每一个线程都单独维护这样一个Hash表. 当ThreadLocal类型对象调用set方法时,这个set方法会使用当前线程维护的Hash表,把本身做为key, set方法的参数做为value插入到Hash表中.因为每一个线程维护的Hash表是独立的,所以在不一样的Hash表中,key值即便相同也是没问题的.

若是不使用static的ThreadLocal变量,那么当定义ThreadLocal的类建立新的实例时候,会出现多个ThreadLocal.这在大多数时候是没有意义的.

内存泄漏问题

ThreadLocal变量被正常回收

ThreadLocalMap使用ThreadLocal的弱引用做为Entry的key,若是一个ThreadLocal没有外部强引用来引用它,下一次系统GC时,这个ThreadLocal必然会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value。

咱们上面介绍的get、set、remove等方法中,都会对key为null的Entry进行清除(expungeStaleEntry方法,将Entry的value清空,等下一次垃圾回收时,这些Entry将会被完全回收)。

可是若是当前线程一直在运行,而且一直不执行get、set、remove方法,这些key为null的Entry的value就会一直存在一条强引用练:Thread Ref -> Thread -> ThreadLocalMap -> Entry -> value,致使这些key为null的Entry的value永远没法回收,形成内存泄漏。

为了不这种状况,咱们能够在使用完ThreadLocal后,手动调用remove方法,以免出现内存泄漏。

ThreadLocal变量没被正常回收

因为咱们通常把ThreadLocal变量声明成static类型,延长了它的生命周期,若是因为一些缘由,持有ThreadLocal变量声明的类没有被回收. 那么确实全部使用这个ThreadLocal变量的线程中的ThreadLocalMap中的entry中的value都不会被回收(key != null). 这确实会形成问题,尤为当你的工做线程是复用的,自己永远不会终止,好比线程池/tomat的工做线程. 更糟糕的是,没法回收的value对应的类自己,和它的类加载器也没法被回收.这个类加载器全部加载的全部的类的数据会存留在永久区.形成永久区内存泄漏(permgen leak)

使用后remove()是一个好习惯.

InheritableThreadLocal

InheritableThreadLocal类是ThreadLocal类的子类. 子线程会copy父线程的值,造成本身的副本(浅拷贝).

1.对于可变对象:

  • 父线程初始化, 由于Thread Construct浅拷贝, 共用索引, 子线程修改父线程跟着变;
  • 父线程不初始化, 子线程初始化, 无Thread Construct浅拷贝, 子线程和父线程都是单独引用, 不一样对象, 子线程修改父线程不跟着变。

2.对于不可变对象: 不可变对象因为每次都是新对象, 因此不管父线程初始化与否,子线程和父线程都互不影响。

从上面两条结论可知,子线程只能经过修改可变性(Mutable)对象对主线程才是可见的,即才能将修改传递给主线程,但这不是一种好的实践,不建议使用,为了保护线程的安全性,通常建议只传递不可变(Immuable)对象,即没有状态的对象。

参考

blog.csdn.net/v123411739/… www.zhihu.com/question/35…

相关文章
相关标签/搜索