ThreadLocal 深入完整解析
一般性的介绍(内存泄漏,基本用法,应用场景,最佳实践等)官网和其他博客都说的很清楚,这里主要记录一下我认为threadlocal的最核心的地方和难点。 主要会包括以下方面:1. 内存泄漏问题,对象引用关系 2. threadLocalHashCode值的选取 3. 深入探究set方法。 注:本文中代码选自jdk8。 其实这两个问题,乃至其他的问题我认为都是对象引用关系,这个是最核心的。 1. 内存泄漏问题,对象引用关系这个图引用自(https://mp.weixin.qq.com/s/bxIkMaCQ0PriZtSWT8wrXw##)架构师修炼宝典 这个公众号。 上图已经将引用关系讲的十分清楚了,由于每个线程对他自己的ThreadLocal持有的引用是放在线程私有的栈里面的,那一旦这个线程丢失了对自己的ThreadLocal的引用之后,如果gc了,那么这个线程里面的ThreadLocalMap里面的对应的Entry的key对ThreadLocal的weakReference将会被回收,那么这里的value值将会变成不可达状态,如果这个entry没有及时被remove掉,那么就会导致内存泄漏问题。 2. threadLocalHashCode值的选取先说说这个是什么 看代码: /** * Get the entry associated with key. This method * itself handles only the fast path: a direct hit of existing * key. It otherwise relays to getEntryAfterMiss. This is * designed to maximize performance for direct hits,in part * by making this method readily inlinable. * * @param key the thread local object * @return the entry associated with key,or null if no such */ private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key,i,e); } 实际上就是每个threadlocal对应的hashCode!看看他的生成原理: private final int threadLocalHashCode = nextHashCode(); 继续 /** * The difference between successively generated hash codes - turns * implicit sequential thread-local IDs into near-optimally spread * multiplicative hash values for power-of-two-sized tables. */ private static final int HASH_INCREMENT = 0x61c88647; /** * Returns the next hash code. */ private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } 就是每个threadlocal的hash值就是i*HASH_INCREMENT+HASH_INCREMENT; i为第i个出引用这个threadLocalHashCode的线程。 注意这里是final修饰的threadLocalHashCode值,每个线程的threadlocal只会有一次赋值的机会。 这个数是魔数,由于每一次扩容都是2的整数次方,用这个算法产生的hash值能够均匀地落到数组的每一个小格子里面,也就是说如果正常流程添加满,是不会冲突的,是可以达到空间效益最大化并且减少hash冲突的。(当然不可能会有加满的时刻,threadlocalMap在被加满前会扩容的) (https://mp.weixin.qq.com/s/bxIkMaCQ0PriZtSWT8wrXw##) 3.深入探究set方法。在我们看最核心的代码,set方法,前先看看简单的get,最终get会调用set的。 /** * Returns the value in the current thread's copy of this * thread-local variable. If the variable has no value for the * current thread,it is first initialized to the value returned * by an invocation of the {@link #initialValue} method. * * @return the current thread's value of this thread-local */ 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(); } 这里的getMaps一开始我还以为是把t作为key值去map里面查,实际上不是的,是返回当前thread持有的threadlocalmap。 /** * Get the entry associated with key. This method * itself handles only the fast path: a direct hit of existing * key. It otherwise relays to getEntryAfterMiss. This is * designed to maximize performance for direct hits,e); } 这里的get就是直接算出hashcode和len-1的与,然后去找到中国entry取值,getEntryAfterMiss这个分支我们先不看。我们先看上面的setInitialValue。 /** * Variant of set() to establish initialValue. Used instead * of set() in case user has overridden the set() method. * * @return the initial value */ private T setInitialValue() { T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this,value); else createMap(t,value); return value; } 这里有一个小地方,就是初始化方法是在第一次调用get的时候触发。 /** * Set the value associated with key. * * @param key the thread local object * @param value the value to be set */ private void set(ThreadLocal<?> key,Object value) { // We don't use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones,in which case,a fast // path would fail more often than not. 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; } //走到以上都是常规逻辑,走到这里就有点奇怪了。 /*其实这里就是为了解决内存泄漏问题的,当走到k==null且entry非空的地方就意味着,弱引用被gc了。因此这个staleentry需要被替换掉。*/ if (k == null) { replaceStaleEntry(key,value,i); return; } } tab[i] = new Entry(key,value); int sz = ++size; if (!cleanSomeSlots(i,sz) && sz >= threshold) rehash(); } 继续看replaceStaleEntry private void replaceStaleEntry(ThreadLocal<?> key,Object value,int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; int slotToExpunge = staleSlot; //循环往前找需要被替换掉的staleslot,直到空为止,然后将最前面的i值赋值给slotToExpunge,表不可能满,肯定不能走完一个循环,下面的同理 for (int i = prevIndex(staleSlot,len); (e = tab[i]) != null; i = prevIndex(i,len)) if (e.get() == null) slotToExpunge = i; //循环往后遍历这个数组 for (int i = nextIndex(staleSlot,len); (e = tab[i]) != null; i = nextIndex(i,len)) { ThreadLocal<?> k = e.get(); if (k == key) { e.value = value; //找到相同的key之后,交换掉i和staleSlot,之后i就是坏节点,是需要被擦除的 tab[i] = tab[staleSlot]; tab[staleSlot] = e; /*如果只有staleSlot前面没有节点,那就把i擦除掉,为什么要这样,可能是因为staleSlot是冲突之后偏移更加短的值,会更加接近真实hash值*/ if (slotToExpunge == staleSlot) slotToExpunge = i; cleanSomeSlots(expungeStaleEntry(slotToExpunge),len); return; } /*如果这个k为空的话把这个节点变成坏节点,为什么要这样,因为staleSlot要么会拿来交换,要么就会拿来建新值,终究会变成好人,但是这个铁定是坑爹的,如果交换了之后,在下面的cleanSomeSlots之后不久交换后的坑爹也会被删掉*/ if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } // 如果找不到相同的key,那就直接赋值 tab[staleSlot].value = null; tab[staleSlot] = new Entry(key,value); // 如果这两个值不等才去删除,因为这样就找到了坑爹货,staleSlot到了这一步都是好人 if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge),len); } 看expungeStaleEntry private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // 能进来的这个方法的staleSlot都铁定是坑爹的。 tab[staleSlot].value = null; tab[staleSlot] = null; size--; Entry e; int i; for (i = nextIndex(staleSlot,len)) { ThreadLocal<?> k = e.get(); //如果发现坑爹货,则删掉 if (k == null) { e.value = null; tab[i] = null; size--; //一直对当前的entry里面的key进行修正位置,(有可能之前的线性探测之后,位置偏移过多,优化表),这里总会遇到空的, 因为表不可能满,会扩容。 } else { int h = k.threadLocalHashCode & (len - 1); if (h != i) { tab[i] = null; // Unlike Knuth 6.4 Algorithm R,we must scan until // null because multiple entries could have been stale. while (tab[h] != null) h = nextIndex(h,len); tab[h] = e; } } } return i; } 最后我们看cleanSomeSlot private boolean cleanSomeSlots(int i,int n) { boolean removed = false; Entry[] tab = table; int len = tab.length; do { //下面的代码是清除操作e.get获取的是key值,也就是threadlocal i = nextIndex(i,len); Entry e = tab[i]; if (e != null && e.get() == null) { n = len; removed = true; i = expungeStaleEntry(i); } } while ( (n >>>= 1) != 0); return removed; } 这个方法一开始看是不太好理解为什么要循环这个次数:(n >>>= 1) != 0。实际上代码注释也说了,这个是一个tradeoff,可以理解为预期。如果遇到的空值了,理解为可能内存泄漏情况严重,而且刚gc过,那就是继续加大预期继续清除。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |
- java – BaseGameUtils无法将GoogleApiClient.Ap
- JavaWeb dbutils执行sql命令并遍历结果集时不能查
- java – Tomcat无法找到我的Servlet并抛出异常,但
- 是否可以使用Azure表存储进行条件插入
- java – HttpServletRequestWrapper,setReadList
- spring学习之创建项目 Hello Spring实例代码
- 在java中的file.lists()中排除系统文件
- Spring 整合Shiro 并扩展使用EL表达式的实例详解
- JSP Request.getServerPath()方法:获取客户端所
- Java语言面向对象编程思想之类与对象实例详解