ThreadLocal源码心得

ThreadLocal源码心得

这篇文章只是写一下自己看ThreadLocal源码时的心得体会,对于具体的源码解析,不做太多的分析,这类文章网上已经有很多了。比如下面这几篇,写的都非常详细:

心得1

ThreadLocal中运用到弱引用的概念,在内部ThreadLocalMap中键是对ThreadLocal实例的弱引用,弱引用的特性是再每次gc时,弱没有其他强引用和软引用,该实例都会被当做垃圾进行回收。这样做是为了防止内存泄漏做的第一重保障,如果不使用弱引用,则该ThreadLocal实例则永远有引用存在,而永远不会被垃圾回收,不论上层应用是否已经将ThreadLocal的引用置空。

源码分析:

首先本人英语一般,由于直译困难,源码中几个重要的概念说明如下:

  • slot:散列表中的一个位置(节点)。
  • full entry和full slot:表示full slot是散列表中一个索引位置。该位置上的Entry称为full entry,其所含的弱引用指向不为null。
  • stale entry和stale slot:stale entry是散列表中stale slot位置的Entry对象,该Entry所含弱引用指向为null。下文中的清理stale entry或者清理stale slot都是意为断开该Entry对象的所有引用,辅助GC,并腾空stale slot位置。
  • null slot:散列表的null slot位置为null,可用于设置新的Entry。
  • run:散列表中任意两个null slot之间的一段,不包括两端。

ThreadLocal的API较为简单,重点以get和set方法为入口,分析流程,只写个人心得,具体分析参见上面链接。

set:

 public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
        //线程已有ThreadLocalMap,调用Map内部方法赋值(重点)
            map.set(this, value);
        else
        //该线程没有绑定ThreadLocalMap,创建一个新的,复制并保存,较为简单
            createMap(t, value);
    }

 ThreadLocalMap.set():

​
private void set(ThreadLocal<?> key, Object value) {

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            
            // i为该键值对应该在的位置,但有可能发生Hash冲突
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                
                // 如果值相同,则更新,返回
                if (k == key) {
                    e.value = value;
                    return;
                }

                // 值为空,则找到了一个stale entry, 内部方法处理entry替换,并扫描,返回
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            //程序运行到此,说明没发生hash冲突,或者冲突了,但到下一个为null的entry,Map中都是                   
            //正常entry,且没有相同key,进行正常赋值,此时i为新entry应该在的index
            tab[i] = new Entry(key, value);
            int sz = ++size;

            //每次赋值操作都会触发一次清理扫描,但这里触发的扫描,是在没发现
            //stale entry的情况下,故乐观处理,扫描范围小
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                
                //如果扫描出stale entry,必然会删除,而每次只能增加一个entry,
                //有删除发生则sz必然小于操作之前,所以有上面的短路与,这里也可以看出大师手笔,扣死每一个可以                                
                //优化的地方。
                rehash();
        }

​

这里出现了两个重要方法:replaceStaleEntry(key, value, i)和cleanSomeSlots(i, sz) :

private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot):

//程序能够进行到此方法,则说明一定发现了stale entry,故在完成替换任务的前提下,要进行一次扫描
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            //从当前位置扫描到前一个null entry,将最前面的stale entry序号记录在slotToExpunge
            //slotToExpunge实际记录的是需要进行扫描的起始位置。
            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;

            // Find either the key or trailing null slot of run, whichever
            // occurs first
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                
                //注意,这个循环是从新增entry应该存在的位置开始的,如果这个if满足,说明需要更新entry,并修改entry的位置。
                if (k == key) {
                    e.value = value;

                    //这个替换操作很巧妙,需要仔细体会引用和实体的概念及关系。赋值操作实际是将=后面的实体,赋给=前面的引用。
                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    //这个if如果成立,说明该stale entry之前(到前一个null)没有stale entry,而该stale entry又经过替换操作,编程一个正常的entry,故可以进一步后移扫描范围(环形队列,没有缩小范围),降低内存泄漏可能
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    
                    //执行扫描操作
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    
                    //到这里程序就返回了,这里就有一个疑问在下面。
                    return;
                }

                //没找到k=key,不进行任何操作,只判断是否需要将起始位置后移。
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            出了循环仍没找到k=key,则就将这个地方赋为正确值
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // 如果slotToExpunge == staleSlot,则说明从前一个null entry 到后一个null entry 只有这一个stale entry, 而这个stale entry也被赋了新值。故不需要扫描。
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

 private boolean cleanSomeSlots(int i, int n)在上面两个方法中都出现了:

private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                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;
        }
}

replaceStaleEntry(key, value, i)方法循环中return引发的疑问:是否会出现这样的情况,导致替换操作不完善:

由于ThreadLocalMap是采用开放定址法(open addressing)解决hash冲突的(另参见分离链表法(separate chaining),hashMap),并且源码该方法的扫描仅仅是在目标位置前后两个null entry之间进行扫描,故有没有可能因为对stale entry的清除而在原有相同key的位置和目标位置之间出现了null entry,而导致扫描无法到达的情况,导致意外,如下图。

答案:我能想到的缺陷,Josh Bloch and Doug Lea两位大师一定能想到,解决方法就在stale entry的清除方法中:

private int expungeStaleEntry(int staleSlot):

private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // 置空,施放资源
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // 对下个null entry之前的entry都进行rehash重新定位
            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;
                    }
                }
            }
            // 返回下个null entry的位置
            return i;
        }

可以看到,每次清除stale entry,都会对每一个entry进行一次rehash直到下一个null entry,这就完全避免了疑问里那种情况的发生,因为清除stale entry后,都会保证所有的entry,和他应该在的位置之间没有null entry存在(又一个大师手笔)。

以上,set的所有流程结束,接下来是get方法(set的几个重要方法清楚之后,get的理解就简单了):

入口:

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();
    }

setInitialValue()方法较为简单,不多说,看map.getEntry(this)方法:

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);
        }
}

其中重要的为getEntryAfterMiss(key, i, e):

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

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }
}

至此,ThreadLocal的所有重要方法分析完毕(重要在内部类ThreadLocalMap中),两个重要的get和set方法流程也走通一遍(还有其他API方法,较为简单,不多说,可以直接看源码)

可以看到,源码中对所有出现stale entry的地方都是非常敏感的,需要重点处理,因为这正是ThreadLocal发生内存泄漏的原因。

待验证疑问:若一个ThreadLocal被多个线程使用,在一个线程中,将ThreadLocal的引用置空,是否影响其他线程中该ThreadLocal的值,感觉是会影响的,因为所有线程的ThreadLocalMap虽然不同,但作为建的ThreadLocal实例都是同一个,如果ThreadLocal被置空,其他ThreadLocalMap中的ThreadLocal键则只有弱引用存在,会被回收。但根据ThreadLocal特性和实际应用,又不会出现上面的情况,所以这个疑问需要事后验证一下。

展开阅读全文

没有更多推荐了,返回首页