JDK源码系列 ThreadLocalMap源码剖析

ThreadLocalMap源码实现

ThreadLocalMap是Thread内部存储ThreadLocal的数据结构,本质上就是一个Map,不过它又和我们熟悉的java.util.map并不太相同,我们来了解一下ThreadLocalMap的具体实现。


1.内部存储结构

ThreadLocalMap的内部存储结构是一个Entry数组,但是它和hashMap不太一样,它没有next指针,说明它不是数组+链表的形式来解决hash冲突,具体往下看。

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

我们来看一下这个Entry内部类的实现,它继承了弱引用WeakReference,key值为ThreadLocal的弱引用,注意,这里存储的ThreadLocal的弱引用,并不是ThreadLocal本身。value值为一个Object类型的强引用类型,用于存放我们需要让ThreadLocal存储的对象。

使用弱引用的原因

首先呢,我们采用普通的kv形式,我们并不能知道ThreadLocalMap外部的强引用是否断开,所以该Entry是否已经被我们弃用我们并不能确定,这样就造成了该Entry的生命周期与线程强绑定在一起,该节点也一直处于GC roots可达状态无法被回收直至线程消亡。即使外部强引用断开,对应的值被我们弃用,我们也无法知道,该无效的Entry无法被清理,这样就会造成资源的浪费。采用WeakReference的形式,我们先来看一下WeakReference的定义,若一个对象没有任何强引用对象绑定,那么该对象下一次GC时无论内存是否足够都会被回收,若某个ThreadLocal没有强引用可达,那么随着它被GC回收,对应的ThreadLocalMap中的Entry的key值也会随之失效,这样就等于给我们一个清理该无效Entry的信号,便于我们的清理工作。


2.ThreadLocalMap的参数
	//Entry数组的初始大小 这里2的幂次 与hashMap类似
    private static final int INITIAL_CAPACITY = 16;
    private ThreadLocal.ThreadLocalMap.Entry[] table;
 	//ThreadLocalMap中存储元素的数量
    private int size = 0;
	//阈值 判断扩容时需要该值
    private int threshold; // 默认为0

ThreadLocalMap内部维护这一个初始大小为16的Entry数组,这里大小2的幂次不多做解释。


3.ThreadLocalMap解决hash冲突的办法

我们继续往下看:

	//调整阈值 保持为负载因子的2/3
    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);
    }		

阈值先不说,和rehash相关,负载因子的概念和hashMap的差不多。
接下来两个函数的作用就是找步长为1的下/上一个索引,这里是环形结构。
记得我们上面说的ThreadLocalMap和HashMap结构不太相同,其实这里就是ThreadLocalMap采取的解决hash冲突的方法,线性探测法
所谓线性探测法,就是根据初始key值的hashCode确定元素在table中的位置,如果发现该位置已经被其他的元素占用了,则利用固定的算法找到一定步长(这里的步长为1)的下一个位置,以此类推,直至找到正确的位置。
线性探测法其实很简单,就不断找位置而已,但也由此可见,该ThreadLocalMap采用线性探测的办法来解决hash冲突的效率是极低的,所以我们不宜用ThreadLocal来存储过多的变量值。


4.ThreadLocalMap api源码剖析
4.1 构造函数
    //ThreadLocalMap是惰性创建的 只有放入第一个元素时ThreadLocalMap才会被创建
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    	//创建一个初始大小为16的Table
        table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
        //找到该元素在table中的位置
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
        //元素个数设置为1
        size = 1;
        //设置阈值 
        setThreshold(INITIAL_CAPACITY);
    }    

该构造函数在createMap中被调用,createMap这函数在ThreadLocal的set()和get()中都出现过。

4.2.关于ThreadLocal的hashCode生成
//当前ThreadLocal的hashCode
private final int threadLocalHashCode = nextHashCode();
//原子类 用于生产当前ThreadLocal
private static AtomicInteger nextHashCode = new AtomicInteger();
//下一个ThreadLocal的hashCode=nextHashCode+HASH_INCREMENT
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
	return nextHashCode.getAndAdd(HASH_INCREMENT);
}

每个ThreadLocal对象都有一个threadLocalHashCode,每初始化一个ThreadLocal对象,hash值就增加一个固定的大小为1640531527。这里0x61c88647即为1640531527,选这个数作为步长对于hash散列是有一定好处的,具体我们就不深究了。

4.3.getEntry函数
	private ThreadLocal.ThreadLocalMap.Entry getEntry(ThreadLocal<?> key) {
        //获取当前元素在table中的位置
        int i = key.threadLocalHashCode & (table.length - 1);
        ThreadLocal.ThreadLocalMap.Entry e = table[i];
        //如果当前位置不为空且命中则直接返回当前结果
        if (e != null && e.get() == key)
            return e;
        else
        	//否则根据线性探测继续找下一个
            return getEntryAfterMiss(key, i, e);
    }
    //如果getEntry没有找到则调用该方法 
    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
         Entry[] tab = table;
         int len = tab.length;
         //基于线性探测的方法往后不断查找直至遇到空的Entry
         while (e != null) {
                ThreadLocal<?> k = e.get();
                //命中返回
                if (k == key)
                    return e;
                //如果为空,说明该Entry已经失效,失效的话就进行expungeStaleEntry处理
                if (k == null)
                    expungeStaleEntry(i);
                else
                	//找下一个索引
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

以上就是getEntry的过程,具体流程如下:

  • 获取当前ThreadLocal的hashCode,通过hashCode&(len-1)取元素在table中的位置
  • 若命中,则直接返回,若e!=null&&e.get()==null,说明该Entry已经失效,我们就需要对其进行清理。
  • 否则,找下一个索引的位置,直至遇到空的Entry。

我们来看一下expungeStaleEntry(i) 这个函数的具体作用:

	private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
			//将value设置为null
            tab[staleSlot].value = null;
            //将当前Entry设置为null help GC
            tab[staleSlot] = null;
            //Map中元素的数量-1
            size--;
           	//从staleSlot的下一个索引开始遍历
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                //若为无效Entry 则按照上面清除无效Entry方式处理
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                	//若为有效Entry
                	//取当前Entry在table中的位置
                    int h = k.threadLocalHashCode & (len - 1);
                    //若该Entry没有发生过hash冲突,则不用重新设置它的位置
                 	//若发送hash冲突 即h!=i 说明发生过线性检测
                    if (h != i) {
                    	//设置当前Entry为null
                        tab[i] = null
                        //从h开始,线性查找第一个为空的Entry 设置为当前Entry的新位置
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            //返回staleSlot之后第一个为空的Entry的位置
            return i;
        }

expungeStaleEntry(i)为清除无效Entry的核心函数,有必要对其流程进行认真地分析。

  • 设置当前无效的Entry.value为null,这样就不会导致无意识的对象保留(这一点在Effective Java将stack的时候讲过,想了解的话可以自己去查阅),设置Entry为null,帮助GC回收无用的内存,将Map中存储元素的个数减少1.
  • 从staleSlot的下一个索引开始遍历,若当前Entry为无效Entry的话,那么就执行上面清除无效Entry的方法。若当前Entry是有效的,那么就判断它是否发生过hash冲突,若h=hashCode&(len-1),且h==i,那么说明该位置上没有发生过hash冲突,就不需要重新设置当前Entry的位置,否则,说明进行过线性检测,那么就从h的位置开始,重新查找当前Entry在table中的位置。(举个例子: 如果当前Entry的h为5,而i却为10,说明5这个位置之前已经被其他元素占用,进行线性检测查找后确定了Entry的位置为10。那么就通过遍历的方法,从5开始重新查找新的位置,这个位置可能的值为5,6,7,8,9,10)。这样子做的目的是为了防止我们清理无效Entry的时候,这个Entry可能(hashCode&(len-1))和Index(线性检测到Entry为空的下标)之间,那么我们查找的时候就会导致我们找到空的Entry就停止,从而找不到后面真正的Entry。
  • 直至遇到空的Entry,退出循环,返回stateSlot之后一个为空的Entry的位置。

以上就是expungeStaleEntry(i)的流程,至于第二步要仔细想想为啥要通过线性探测重新定位有效Entry的值。

4.4.set方法

接下来就是分析一下set方法的源码:

		private void set(ThreadLocal<?> key, Object value) {
            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)]) {
                ThreadLocal<?> k = e.get();
                //命中 设置新的值 直接返回 
                if (k == key) {
                    e.value = value;
                    return;
                }
				//若为无效的值 我们需要对其进行替换
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //若没有对应的Entry,则新建一个Entry
            tab[i] = new Entry(key, value);
            int sz = ++size;
            //如果进行启发式地清理没有发现无效的Entry且当前的数量大于阈值,则进行reHash操作
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

set方法的流程就是:

  • 进行线性检测,若发现目标key,则设置新值后直接返回,若发现无效的Entry的话,则进行替换。
  • 如果没有发现目标key,也没有无效的Entry,则直接新建一个Entry,并且Map存储元素的个数加一,并且会进行一次启发性的清理,若这次启发性清理函数没有清理任何无效的Entry且当前包含元素的个数超过阈值,那么我们就进行一次rehash()操作。

我们来看一下replaceStaleEntry方法:

	private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;
        	//当前无效Entry的位置
            int slotToExpunge = staleSlot;
            //从staleSolt的上一个索引通过线性检测进行遍历 直至遇到空的Entry
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                 //如果找到失效的值 则设置slotToExpunge为当前失效Entry的位置下标
                 if (e.get() == null)
                    slotToExpunge = i;
			//从staleSolt的下一个所有通过线性检测进行遍历 直至遇到空的Entry
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
				//如果找到我们需要的目标key
                if (k == key) {
                    e.value = value;
					//与失效Entry进行交换
                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;
                    //如果staleSlot向前进行遍历找不到无效的Entry
                    //那么就把slotToExpunge设为当前i
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    //expungeStaleEntry不再多说,就是从slotToExpunge开始进行遍历清除无效的Entry
                    //而cleanSomeSlots就是做启发性的清理 这段在后面讲
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }
				//如果当前的Entry无效 且之前的向前扫描没有扫描到无效的Entry
				//那么就把slotToExpunge设为当前i
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }
           	//若在table中找不到目标key 则将无效Entry的value置为null
            tab[staleSlot].value = null;
            //把无效的Entry直接变成我们需要设置的Entry
            tab[staleSlot] = new Entry(key, value);
            
          	
          	//在探测过程中若发现任何无效的Entry 对其进行清理(连续性清理+启发式清理)
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

我们将一下replaceStaleEntry的流程:

  • 先从staleSolt的前一个索引开始遍历,直至遇到空的Entry,若发现无效的Entry的,则更新slotToExpunge的值。
  • 从staleSolt的下一个索引开始遍历,若找到目标key,则将之前set方法检测到的无效的Entry和存储目标key的Entry的进行替换,若上个步骤没有找到无效的Entry,那么就更新slotToExpunge的值为当前的位置i(已经替换成无效的Entry了),并且进行一次连续性的清理(expungeStaleEntry)加上启发性的清理(cleanSomeSlots),直接返回。若发现无效的Entry,且上个步骤没找到无效的Entry,则更新slotToExpunge的值为当前位置的i,直至遇到空的Entry,退出循环。
  • 若没有找到目标key,说明目标key已经失效,那么就直接在之前set方法检测到的无效的Entry的位置上新建一个我们需要设置的Entry。
  • 若在探索过程中发现任何无效的Entry,就进行一次连续性的清理(expungeStaleEntry)加上启发性的清理(cleanSomeSlots)。

我们来看一下cleanSomeSlots这个方法:

		private boolean cleanSomeSlots(int i, int n) {
            //n为当前Map的容量大小 用来控制下面循环的次数
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do 
            	//因为i对应的Entry非无效Entry 所以我们从下一个索引开始
                i = nextIndex(i, len);
                Entry e = tab[i];
                //若该Entry无效
                if (e != null && e.get() == null) {
                	//扩大扫描因子
                    n = len;
                    removed = true;
                    //进行连续性清理
                    //且返回值为i之后第一个为空的Entry的位置下标
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            //若log(n)次后没有发现无效的Entry 则退出循环
            return removed;
        }

上面这个函数为启发性地清理无效的Entry,主要两个地方运用到,1、set()的时候有用到该函数,2、进行无效Entry替换的时候有用到该函数,且传入i的值都为非无效Entry的下标(可能为空,可能为有效的Entry),这就是上面代码为什么从i=nextIndex(i, len)开始。
该函数所做的工作就是发现无效的Entry,并进行连续性清理(expungeStaleEntry),若循环log(n)次之后都没发现无效的Entry的话,则退出循环。

最后我们看一下rehash这个函数的源码:

		private void rehash() {
			//对整个table进行一次全量清理
			expungeStaleEntries();
			//如果容器存储的个数超过阈值的3/4的话,就进行一次扩容
   			if (size >= threshold - threshold / 4)
   				resize();
   			}
		}
		private void expungeStaleEntries() {
            Entry[] tab = table;
            int len = tab.length;
            //遍历table,对每一个无效的Entry都进行一次连续性清理
            for (int j = 0; j < len; j++) {
                Entry e = tab[j];
                if (e != null && e.get() == null)
                    expungeStaleEntry(j);
            }
        }
        //扩容函数 其实就是容量扩大2倍
        private void resize() {
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            int newLen = oldLen * 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;
        }

以上就是set()调用到的相关函数,其实我们只要知道set()的时候会顺带清理无效的Entry即可,而且在清理无效的
Entry的同时,若存储的元素数量超过阈值的3/4,就会进行扩容操作。

4.5.remove()

最后,就是ThreadLocalMap的最后一个方法:

		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) {
                	//设置referent为null
                    e.clear();
                    //进行一次连续性清理
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

remove方法其实很简单,设置弱引用reference为null,再进行一次连续性清除。
有没有发现,ThreadLocalMap的getEntry()、set()、remove()都可以清除无效的Entry.
即设置Entry.value=null,Entry=null,而Entry.reference,前两个方法是被gc自动回收的,remove是手动设置为null的。


5. 关于ThreadLocal内存泄露

思考中…(待写…一方面是线程池,一方面是自己的清理机制)

参考: ThreadLocal源码解读

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值