likes
comments
collection
share

ThreadLocal的使用及原理

作者站长头像
站长
· 阅读数 3

引言

ThreadLocal的使用及原理

ThreadLoacl 是一个线程内部的数据存储结构,可以理解为每个线程对象都有一个私有的数据存储集合。它以线程对象为界限来存储数据,该数据存储后,只有在指定的线程才能获取到存储的数据。

使用场景:某些数据以线程为作用域,不同线程数据相互独立,可以考虑使用 ThreadLoacl

ThreadLocal的使用及原理

基本使用

main 方法内部创建了一个 ThreadLocal 变量,分别在主线程和子线程中获取该值。

public static void main(String[] args) {                                                  
    ThreadLocal<String> mThreadLocal = new ThreadLocal<>();                               
    mThreadLocal.set("tsp");                                                              
    new Thread(() -> {                                                                    
        String content = mThreadLocal.get();                                              
        System.out.println(Thread.currentThread().getName() + " = " + content);           
    }, "Thread#1").start();                                                               
    new Thread(() -> {                                                                    
        mThreadLocal.set("tt123");                                                        
        System.out.println(Thread.currentThread().getName() + " = " + mThreadLocal.get());
    }, "Thread#2").start();                                                               
    System.out.println(Thread.currentThread().getName() + " = " + mThreadLocal.get());    
}

//输出如下:
// Thread#1 = null
// main = tsp
// Thread#2 = tt123

可以看到:只有当前 mian 的线程才能获取到该值,Thread1Thread2 获取 mThreadLocal 都为 null

实现原理

首先来看一下 ThreadLocal.get() 方法:

public T get() {
    //1.首先获取当前运行的线程
    Thread t = Thread.currentThread();
    //2.调用 getMap 方法传入当前线程实例,获取当前线程对象的  ThreadLocalMap 变量
    ThreadLocalMap map = getMap(t);                  
    if (map != null) {
        //4.这个 this 将 ThrealLocal 传递进去,得到 ThreadLocalMap.Entry
        // 所有的 ThreadLoacl 都保存在这个 map 中, key 为 ThreadLocal<?>,值为对应的 value
        ThreadLocalMap.Entry e = map.getEntry(this); 
        if (e != null) {                             
            @SuppressWarnings("unchecked") 
            //从 entry 中取得值就是保存的值
            T result = (T)e.value;                   
            return result;                           
        }                                            
    }
    //3.如果当前线程的 ThreadLocalMap 为 null,初始化
    return setInitialValue();                        
}

ThreadLocalMap getMap(Thread t) {
    //thread中维护了一个 ThreadLocalMap 的成员变量
    return t.threadLocals;
}

//初始化ThreadLocal Map
private T setInitialValue() {
    //初始化值为 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;                       
}                                       

//不存在则创建,这个 threadLocals 就是线程的局部变量,所有对 ThreadLocal 的操作都是对这个 map 进行操作
void createMap(Thread t, T firstValue) {                      
    t.threadLocals = new ThreadLocalMap(this, firstValue);    
}                                                             

get() 方法流程很简单,首先获取当前线程对象 t ,然后通过 getMap(t) 获取当前线程 tThreadLocalMap 对象,如果不为 null, 则从 map 中获取 ThreadLocalMap.Entry ,不为 null 则返回,否则执行 setInitialValue() 初始化。

接着来看一下 ThreadLocalMap 是个啥:

ThreadLocalMapThreadLocal 的一个静态内部类,从名称也可以知道它是以 key:value 的形式存在,节点信息如 Entry ,如下:

//静态内部类 Entry 节点,key 为一个弱引用持有的 ThreadLocal,value 为 对应的值。
static class Entry extends WeakReference<ThreadLocal<?>> {                             
    Object value;                                                                                         
    Entry(ThreadLocal<?> k, Object v) {                                
        super(k);                                                      
        value = v;                                                     
    }                                                                  
}                                                                      

注意:这样设计的好处是,如果这个变量不再被其他对象使用时,可以自动回收这个 ThreadLocal 对象,避免可能的内存泄露(注意,Entry 中的 value,依然是强引用)。

ThreadLocalMap 初始化如下:

//ThreadLocalMap构造方法如下,
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    //内部成员数组,INITIAL_CAPACITY值为16的常量
    table = new Entry[INITIAL_CAPACITY];
    //位运算,结果与取模相同,计算出需要存放的位置
    //threadLocalHashCode比较有趣
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
		size = 1;
    //这是负载因子为容量的 2/3 
	setThreshold(INITIAL_CAPACITY);
}

每个线程Thread都持有一个Entry型的数组table,而一切的读取过程都是通过操作这个数组table完成的。

ThreadLocal的使用及原理

ThreadLocalMap.set() 操作:

private void set(ThreadLocal<?> key, Object value) {                                                     
    Entry[] tab = table;                                                                       
    int len = tab.length;                                                                      
    //1.hash值获取:通过操作一个原子类变量每次增加一个固定的值来得到一个 hashCode                                   
    //与 tab 长度 len 相 & 计算在tab 中的位置                                                             
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];                                                                     
         e != null;                                                                            
         //2.每次取 tab 的下一个位置。                                                                   
         e = tab[i = nextIndex(i, len)]) {                                                     
        ThreadLocal<?> k = e.get();                                                            
        //3.如果当前 tab 中的 key 相等,则更新值                                                            
        if (k == key) {                                                                        
            e.value = value;                                                                   
            return;                                                                            
        }                                                                                      
        //4.如果当前 k 为 null  ,则释放当前 entry 资源,这也是在每次设置时候,会将key为null的entry节点释放掉 
        if (k == null) {                                                                       
            replaceStaleEntry(key, value, i);                                                  
            return;                                                                            
        }                                                                                      
        //5.如果 key 与当前 k 不相等,即出现 hash 冲突,就在 tab 中查找下一个位置,这也是 ThreadLocalMap 在遇到 hash 冲突时候,会将当前 entry 存储到 tab 的下一个位置                                                            
    }                                                                                          
    //6.到这里说明当前 tab 中不存在,创建                                                                    
    tab[i] = new Entry(key, value);                                                            
    int sz = ++size;                                                                           
    //7.首先清除一些Entry中key为 null 的节点,更新大小,如果还是大于负载因子,则进行扩容                             
    if (!cleanSomeSlots(i, sz) && sz >= threshold)                                             
        rehash();                                                                              
}                                                                                              

ThreadLocalMap.get() 操作:

public T get() {                                                        
    Thread t = Thread.currentThread();                                  
    ThreadLocalMap map = getMap(t);                                     
    if (map != null) {                                                  
        //1.如果存在,则直接返回。                                                 
        ThreadLocalMap.Entry e = map.getEntry(this);                    
        if (e != null) {                                                
            @SuppressWarnings("unchecked")                              
            T result = (T)e.value;                                      
            return result;                                              
        }                                                               
    }                                                                   
    //2.否则初始化后返回,默认初始化为 null,然后通过 set(key,value) 设置值,这里返回 null          
    return setInitialValue();                                           
}                                                                       

问题梳理

虽然 ThreadLocalMap 中的 key 是弱引用,当不存在外部强引用的时候,就会自动被回收,但是 Entry 中的value依然是强引用。这个value的引用链条如下:

ThreadLocal的使用及原理

可以看到,只有当Thread被回收时,这个value才有被回收的机会,否则,只要线程不退出,value总是会存在一个强引用。但是,要求每个Thread都会退出,是一个极其苛刻的要求,对于线程池来说,大部分线程会一直存在在系统的整个生命周期内,那样的话,就会造成value对象出现泄漏的可能。处理的方法是,在ThreadLocalMap进行set(),get(),remove()的时候,都会进行清理:

getEntry()为例:

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        //如果找到key,直接返回
        return e;
    else
        //如果找不到,就会尝试清理,如果你总是访问存在的key,那么这个清理永远不会进来
        return getEntryAfterMiss(key, i, e);
}

getEntryAfterMiss()的实现:

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        // 整个e是entry ,也就是一个弱引用
        ThreadLocal<?> k = e.get();
        //如果找到了,就返回
        if (k == key)
            return e;
        if (k == null)
            //如果key为null,说明弱引用已经被回收了
            //那么就要在这里回收里面的value了
            expungeStaleEntry(i);
        else
            //如果key不是要找的那个,那说明有hash冲突,这里是处理冲突,找下一个entry
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}
//真正用来回收value的是expungeStaleEntry()方法,在remove()和set()方法中,都会直接或者间接调用到这个方法进行value的清理:
 private int expungeStaleEntry(int staleSlot) {                                 
     Entry[] tab = table;                                                       
     int len = tab.length;         
     tab[staleSlot].value = null;                                               
     tab[staleSlot] = null;                                                     
     size--;                                                                                             
     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;                                                       
                 while (tab[h] != null)                                         
                     h = nextIndex(h, len);                                     
                 tab[h] = e;                                                    
             }                                                                  
         }                                                                      
     }                                                                          
     return i;                                                                  
}                                                                              

从这里可以看到,ThreadLocal为了避免内存泄露,也算是花了一番大心思。不仅使用了弱引用维护 key,还会在每个操作上检查key是否被回收,进而再回收 value

但是从中也可以看到,ThreadLocal并不能100%保证不发生内存泄漏。你的get()方法总是访问固定几个一直存在的ThreadLocal,那么清理动作就不会执行,如果你没有机会调用 set()remove(),那么这个内存泄漏依然会发生。

因此,一个良好的习惯依然是:当你不需要这个ThreadLocal变量时,主动调用remove(),这样对整个系统是有好处的

2.ThreadLocal解决Hash冲突问题

我们知道,HashMap 中出现 Hash冲突时候,会用链表法来解决冲突,而对于 ThreadLocalMap,它使用的是简单的线性探测法,如果发生了元素冲突,那么就使用下一个槽位存放:

ThreadLocal的使用及原理

结语

ThreadLocal 能做到线程间数据隔离,因为每个线程对象都有一个 ThreadLocalMap 类型的成员变量,setget 操作都是操作这个 map,每个线程对象都有自己独立的 map,所以也就不存在相互访问的问题了。