第十八章 Java线程之ThreadLocal源码分析

古语有云

不积跬步,无以至千里。
不积小流,无以成江海。
尽管我们感觉现在的时代生活越来越不容易,尽管世界充满着:“还在相信只要努力就会成功吗?”。
从历史的长河来看,我们如今的时代不过是沧海中的一粟。那些经过上千年沉淀和传承下来的智慧依然是人生中正确的导向。
大家好,我是中国茫茫几百万程序员大军中的一员,也是非计算机专业大专IT培训出生的程序员,在迷惘和失落的时候,心中依然坚信着老祖宗传下来的智慧:
契而舍之,朽木不折。
锲而不舍,金石可镂。

前言

在做一篇文章分析之前,总会做大量的功课。这篇文章也是一样,最开始看到ThreadLocal,我并不明白它的好处在哪里。网上文章一大堆的说着看似正确却又不正确的结论,于是我打算自己来搞明白。
ThreadLocal的作用,在我的理解中它就是一个key。而线程可以是一个存储容器,当我们需要把东西存入一个线程对象里面的时候,就需要用ThreadLocal当成一把key,需要存储的目标值为value。类似HashMap一样,将这对key-value组成的键值对存入线程里。
目前能理解到它的好处在于,线程隔离,重点在于隔离两个字。我们可以通过它将目标数据储存在每一个的线程里面隔离保护,实现线程范围内的方法间的变量传递。

结构

本章将会从设计源头上讲起,为什么要设计,要怎样设计,设计方案有什么好处和坏处。怎样处理设计上带来的坏处。
总体思路为:为什么要做(目的),要怎样去做(实践),怎样做到最好(方法)。

线程中的容器

目前个人理解,如果线程中拥有了自己的容器,那么可以安全的隔离保护数据,多线程环境下,不会被其他线程干扰。
因此,为了使线程拥有一个存储数据的功能,那么不可避免的是线程内部必须要有一个存储容器,不论是List、Map等等。如果选用List,对于不同变量关联不同类型的对象时,显然控制起来是复杂的。而Map通过key-value的特点可以很好的满足这种特性需求。当线程具有了容器功能,那么在线程生命周期内,不论进行到哪个方法,都可以获得这个Map,并取出里面的值。

在这里插入图片描述
如上图所示,线程每调用一个方法,便会压入一个栈帧,这些栈帧与Map都在同一个栈内,所以不论在线程运行到哪个方法,都可以调用Map对象,它解决了方法间的数据传递,隔离了多线程环境中的并发安全问题。

用过HashMap的人都知道,它是key-value键值对形式组成的。现在线程中有了Map,并且我们要存入一个目标value值,那么key用什么来标记呢?ThreadLocal对象就是解决这个问题,一个ThreadLocal对象 对应一个 value。
那么可以怎么用呢?与传统方法相比有什么好处呢?

用法

如果要解决方法间的变量传递,那么有两种情况:
1. 同一个类中方法间的变量传递
2. 不同类中方法间的变量传递

针对第一个问题,传统方法用成员变量就可以解决了。

比如上图中要在方法1和方法2中共享一个变量,那么我们定义一个成员变量就可以解决了。在方法1中赋值,方法2中取值。

如果用ThreadLocal可以这样使用。
在这里插入图片描述
这个也实现了在方法1中赋值,在方法2中取值。

针对第二个问题,如果是在不同类中的方法间数据传递,传统方法需要怎么实现呢?

在这里插入图片描述
在已有的调用链中,我们可能需要将参数一层一层的向下传递,直至到达目标方法。 如果你说用static静态变量也可以解决,但是这样无疑会产生多线程安全问题。
如果是用ThreadLocal怎么实现呢?

这时只需要获得threadLocal的这个key,就可以在其他地方获取当前线程内用这个key存储的数据。

对于ThreadLocal而言,为什么官方建议用private修饰?
如果用private修饰,那么这个线程生命周期内值传递的作用范围仅限于这个类中了,对于其他类而言,就无法通过通过这个key来获取value了,这样的话与创建一个成员变量还有什么区别呢?我觉得它更大的作用在于不管在哪个类中,只要在线程栈方法调用范围内,都可以获取到才对。
所以我觉得使用private提供了对变量的保护,而获取ThreadLocal对象我们可以编写一个get方法获取,整个线程栈的其他方法都可以通过获取这个ThreadLocal对象,来获得对应的value值。

上面体会到了ThreadLocal的用法和好处,可以在任何地方获取线程内存储的变量值,实现了方法间的变量传递,实现了多线程之间的绝对隔离。
下面来剖析它们的组成成分。这个Map是长成什么样子的?

ThreadLocal.ThreadLocalMap

threadLocalMap 是ThreadLoca类中的一个内部类,是专门为线程设计的一个具有map特点的容器,这个ThreadLoca可以和HashMap进行类比。

  1. 它们的底层都是通过数组实现的
  2. 它们都是通过hash算法来定位的
  3. 它们都有相同的优点和缺陷

在Thread类中持有了成员变量ThreadLocal.ThreadLocalMap,因此让线程实现了容器的功能。
那么该容器的实现细节是怎样的?

一、ThreadLocalMap

这个类是ThreadLocal的内部类,但是ThreadLocal并不持有这个变量。意思就是,生产它却不持有它。
首先来看下ThreadLocalMap的变量组成。

	// 它有一个Entry数组
    private Entry[] table;
	// 数组的初始化容量是16,并且必须是2的幂
	private static final int INITIAL_CAPACITY = 16;
	// 这个变量计量着实际元素个数
    private int size = 0;
	// 和HashMap一样的负载因子,默认为0;
    private int threshold; 

从变量组成来看,它和HashMap的内部组成并无差异,都是维护着一个Entry数组,不过这两个Entry数组并不一样。

static class Entry extends WeakReference<ThreadLocal<?>> {
       
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }

从Entry的构成中可以看出,它以ThreadLocal对象作为Key,并且实现了一个弱引用。Object就是实际要存储的值。

弱引用与内存泄漏

如果不设计成弱引用,那么每个线程中的ThreadLocalMap的Entry会强引用ThreadLocal对象。
在这里插入图片描述
如上图所示,也就意味着这个ThreadLocal对象的生命周期和引用它时间最长的那个线程的生命周期一致了。而常常实际应用中的线程池中,核心线程会和应用的周期一样长,从而导致了线程中的Map对ThreadLocal一直保持引用,ThreadLocal对象无法释放,造成了内存泄漏。
而如果是设计成弱引用,那么线程的生命周期再长也不会一直保持对ThreadLocal对象的引用,ThreadLocal对象会自己随着生命周期阶段该回收时就被回收了。
如果ThreadLocal对象被回收后,那么这些线程对它的引用就会变成了null了,但是ThreadLocal对象作为key对应Value值却还在线程中,如果线程一直存活,那么对于Value值来说,它也是很容易引起内存泄漏的。(想想许多线程都挂载着一个无用的Value都可怕,内存不泄露才怪!)

那怎么办呢?

remove()方法

如果上述问题可能产生内存泄漏,那么这个方法就是为防止内存泄漏而诞生的。看看它做了什么。

 public void remove() {
     ThreadLocalMap m = getMap(Thread.currentThread());	// 获取当前线程中挂载的map
     if (m != null)
         m.remove(this);								// 调用ThreadLocalMap 的remove方法
 }

// 下述为m.remove(this)的调用方法
private void remove(ThreadLocal<?> key) {
        Entry[] tab = table;		// 获取map中哈希桶
        int len = tab.length;
        int i = key.threadLocalHashCode & (len-1);  // 算出当前ThreadLocal对象在这个map中的存放位置
        
        //下方循环中,为了解决哈希冲突,采用的线性查探法。所以定位到的位置上可能不是当前ThreadLocal对象
        对应的Entry。 所以加了判断如果该位置上的ThreadLocal对象内存地址和传入的key相等,才能确定位置。
        for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
            if (e.get() == key) {			
                e.clear();		
                expungeStaleEntry(i);		// 找到之后调用了这个方法
                return;
            }
        }
    }

// 继续往下看
private int expungeStaleEntry(int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;

        // expunge entry at staleSlot
        tab[staleSlot].value = null;		// 这里释放了value对象。由于key是弱引用会自动收回,所以无需手动释放。
        tab[staleSlot] = null;				// 然后释放了entry结点。 
        size--;
	
	 	// 按理说可以结束了,但是下面它继续在释放。
        // Rehash until we encounter null
        Entry e;
        int i;
        // 这里循环遍历下一个结点,查看它的key是否为null,
        for (i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {
            ThreadLocal<?> k = e.get();
            if (k == null) {		// 也就是如果下一个key的ThreadLocal对象已经被回收了,那么它的value也应该被回收
                e.value = null;
                tab[i] = null;
                size--;
            } else {
            // 如果下一个结点存在,则判断是否需要做位置校正。这是什么意思呢?
                int h = k.threadLocalHashCode & (len - 1);  
                if (h != i) {		// 如果这个结点计算出来的原定位为h,现在却是了i这个位置,说明这个结点是因为哈希冲突被顺延过来的。现在你前面的位置空出来了,你该回去了。 
                    tab[i] = null;		// 这里就说,这个位置腾出来吧,那腾出来了,这个Entry去哪儿呢 ?

                    // Unlike Knuth 6.4 Algorithm R, we must scan until
                    // null because multiple entries could have been stale.
                    // 于是这个entry又从原始位置h出开始重新定位,如果哈希冲突则顺延,不过它肯定不会再回到原来的位置上了。因为它原来位置前方肯定有空着的位置。
                    while (tab[h] != null)
                        h = nextIndex(h, len);
                    tab[h] = e;
                }
            }
        }

可以总结的是,remove方法做了什么事情。

  1. 释放了当前ThreadLocal对象绑定的value对象,并释放了map中响应的Entry结点。
  2. 检查这个位置后继结点是否存在其他ThreadLocal为null的现象,如果存在则释放。
  3. 如果后续位置中,存在因为哈希冲突顺延的结点,则对该结点重新定位。

为什么建议private static

这个缘由来自把ThreadLoad对象用static修饰。官方建议是threadLocal实例应该是private static的,这样可以节约内存消耗,避免重复的ThreadLocal对象生成。如果ThreadLoca对象是一个非静态变量,那么意味着每一个外部对象的产生,就意味着产生一个ThreadLocal。线程对不同外部对象的调用,那么访问到的ThreadLocal也不是同一个,除非外部对象是一个单例。当然这也不会造成什么异常,但是会增加内存的开销。
所以如果我们用static修饰,那么只会产生一个ThreadLocal对象,这也足够用了。
但是这样也有缺点,static变量对ThreadLocal对象保持着强引用,而static 变量从类加载的时候创建完成后会一直存在,这也会导致强引用会一直存在,从而有产生内存泄漏的危险,即使线程的Map中Entry对象对它保持着弱引用。这是什么意思呢?

在这里插入图片描述
既然使用private static会造成内存泄漏,那为什么官方还要这样建议呢?如果这样做,那官方相近办法的设计弱引用还有什么意义呢?
我的个人想法是,这样做会产生内存泄漏是肯定的,但是只有一个对象的占用,相比产生许多ThreadLocal对象更能够节约内存吧。另外一点就是,在不使用static修饰的情况下,也可以达到相同的效果,比如外部对象是一个单例时,ThreadLocal对象也只有一个。所以设计成弱引用依然有它的作用。

get()方法

 public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);		// 获取线程内部的map
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);		// 如果不是null则获取结点
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;		//返回value值
        }
    }
    return setInitialValue();  // 否初始化一个新的map,并创建结点,value初始值为null
}

set()方法

 public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);		// 获取线程内部map
    if (map != null)
        map.set(this, value);		// 在现有map中添加结点
    else
        createMap(t, value);		// 或者初始化一个map
}

ThreadLocalMap特点

(1) 只有数组结构
ThreadMap中维持着一个数组table,意味着它拥有数组结构。结点的定位也是依靠Hash算法定位,与HashMap不同的是,ThreadLocalMap中的Entry结点只有key-value结构,没有前后结点引用,所以它不存在链式结构。那么它是怎么处理Hash冲突的呢?

(2) 开放地址法之线性查探
ThreadLocalMap采用的开放地址法的线性查探,如果通过哈希定位计算出的位置上已经有结点存在了,那么向后顺延就好了,如果后面还有,则再向后延,直到找到为止。

是否存在找完了都没有合适的位置吗?
不会的,因为ThreadLocalMap也有负载因子,默认负载因子为 len * 2/3;

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

扩容以后,原哈希桶中的元素怎么散列?

private void resize() {
        Entry[] oldTab = table;
        int oldLen = oldTab.length;
        int newLen = oldLen * 2;		// 扩容倍数被2倍
        Entry[] newTab = new Entry[newLen];
        int count = 0;

        for (int j = 0; j < oldLen; ++j) {
            Entry e = oldTab[j];
            if (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null; // Help the GC		// 再散列前,先释放无效的结点。
                } else {
                    int h = k.threadLocalHashCode & (newLen - 1);	// 用新长度对每个结点重新定位
                    while (newTab[h] != null)
                        h = nextIndex(h, newLen);
                    newTab[h] = e;
                    count++;
                }
            }
        }

        setThreshold(newLen);
        size = count;
        table = newTab;
    }

总结

整个ThreadLocal 大概就是这么些了,有阐述不正确的,请指点一下,有疑惑的地方也可以一起讨论。

猜你喜欢

转载自blog.csdn.net/weixin_43901067/article/details/106504597
今日推荐