Threadlocal

目录

从名称上来看可以理解为线程本地变量,也可以认为是线程局部变量,线程与线程之间都是隔离的,所以说也是线程安全的,是典型的空间换时间的设计理念

一、基本结构

先看一下该类的重要成员和重要的内部类:

/** 以下三个不用管,只需要记住是用来计算类的Hash的就可以了 */
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
// 用来保证hash更均匀
private static final int HASH_INCREMENT = 0x61c88647;

// 重点内部类(内容省略,只需要记住是个Map)
static class ThreadLocalMap {}


上面最关键的就是ThreadLocalMap 这个Map,其他三个都是用来计算Threadlocal自身的hash的,因为在ThreadLocalMap 里面Key 就是Threadlocal自身,所以综合下来线程Thread与**ThreadLocalMap **关系如下:

每个线程都会持有一个ThreadLocalMap, Threadlocal只是里面的key而已,一个线程要是有多个变量那应该是这样:

ThreadLocalMap和线程Thread是如何关联的呢?注意我图示Thread下面的**threadLocals了嘛,线程类内部本身就有threadLocals这个属性,这个属性就是ThreadLocalMap **,如下:

二、ThreadLocal操作

经过上面相信大家已经对ThreadThreadLocalMapThreadLocal三者结构有一定了解了,那ThreadLocal到底干了些什么?其实就是把自身当做key,作为一个中间者来交互,实现增删改操作

set操作

逻辑很简单:

  • 从线程中获取map,获取到了就把自身作为key加上值 写入到map中
  • map不存在就重新创建一个给线程,并写入key-value

源代码如下:

public void set(T value) {
    Thread t = Thread.currentThread();
    // 获取当前线程里面的ThreadLocalMap 
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // map不为空 就把自身当做key,从map里面取值
        map.set(this, value);
    else
        // map为空则为线程创建一个map 并写入值
        createMap(t, value);
}

// 为线程创建一个map
void createMap(Thread t, T firstValue) {
    // 创建一个map赋值给线程的threadLocals属性
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
// 线程获取Map就是获取线程的threadLocals属性
ThreadLocalMap getMap(Thread t) {
  return t.threadLocals;
}

get操作

也是一样从线程中获取map,然后从以自身为key从map中获取value

如果map不存在则会新建一个map,set 一个null值,并返回null值

源代码如下:

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // map存在 则以自身为key 从map中获取value
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // map不存在 则创建一个新的map 并返回null
    return setInitialValue();
}
// 初始化map
private T setInitialValue() {
    // 这个值就是null 下面返回的也是null  set的值也是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;
}

remove操作

移除也简单就是以自身为key,移除值

源代码如下:

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

三、内存泄露?

上面三个操作基本上就是ThreadLocal全部了(下面说map的),ThreadLocal有关最多的话题就是内存问题,难道用ThreadLocal很容易发生内存溢出吗?

个人觉得ThreadLocal跟内存溢出没关系,从根本上看ThreadLocalMap 是作为属性与Thread绑定的,Thread不结束,map的引用就不会释放,所以内存不会被回收,本质上是线程没结束,内存释放不了;从使用角度上来讲,我线程没结束,我也意识到了要清除map里面部分值来释放内存,但是突然发现清不了,因为key没了,value还存在,为什么会这样?

因为ThreadLocal作为key是弱引用,可以被回收,而value是强引用,试想你key都没了,还怎么移除value?

ThreadLocalMap内Entry定义如下,继成了WeakReference :

所以说如果Thread能很快的正常结束,那无事发生,如果Thread的生命周期很长,长时间存在的那种,那么在使用ThreadLocal的时候就要注意,不用的变量要及时的remove掉;线程不都很快结束吗?什么线程会长时间存在?想想线程池的核心线程

四、ThreadLocalMap

关于这个map我就不分操作一个一个说了,说几个关键的

核心变量

// 默认大小
private static final int INITIAL_CAPACITY = 16;

// 数组       
private Entry[] table;

// 元素数量
private int size = 0;

// 扩容因子
private int threshold; 

底层数据结构就是数组,但没有链表了,因为不是以链表的形式来解决hash冲突的

数组下标计算方式

threadLocalHashCode & (length-1)

threadLocalHashCode:就是开头说的ThreadLocal那三个来计算得出的

length:数组长度

阈值计算

数组长度的2/3

源代码如下:

private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

扩容

  • 双倍扩容
  • 遍历之前的key-value,重新计算key的下标,然后放入新数组

源代码如下:

private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    // 扩容两倍
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;
    
    // 遍历之前的数组 重新计算之前key的hash 在写入新数组
    for (int j = 0; j < oldLen; ++j) {
        Entry e = oldTab[j];
        if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                // 如果key为null  则把value也置为null 
                e.value = null; // Help the GC
            } else {
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                    // 如果下标已经有值了(hash冲突了) 则往后找空位
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }
    // 重新计算阈值
    setThreshold(newLen);
    size = count;
    table = newTab;
}

下标冲突(hash冲突)

与HashMap不同,这个是用开放寻址法(线性探测法),就是说如果当前下标冲突了,就往后找空位,直至找到一个空位放入

源代码如下:

// 传入长度和下标索引
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}
Last Updated: