线程局部变量ThreadLocal实现原理

之前做项目用到过ThreadLocal,但是没有看源码层面的具体实现,今天特来补一补课。

ThreadLocal,即线程局部变量,用来为每一个使用它的线程维护一个独立的变量副本。

这种变量只在线程的生命周期内有效。并且与锁机制那种以时间换取空间的做法不同,ThreadLocal没有任何锁机制,它以空间换取时间的方式保证变量的线程安全

 

1、ThreadLocal类图结构

 

 

 

SuppliedThreadLocal:主要是JDK1.8用来扩展对Lambda表达式的支持,有兴趣的自行百度。

ThreadLocalMap:是ThreadLocal的静态内部类,也是实际保存变量的类。

Entry:是ThreadLocalMap的静态内部类。ThreadLocalMap持有一个Entry数组,以ThreadLocal为key,变量为value,封装一个Entry。

 

2、Thread,ThreadLocal,ThreadLocalMap和Entry的关系

 

 

 说明:

  1. 一个Thread拥有一个ThreadLocalMap对象;
  2. 一个ThreadLocalMap拥有多个Entry数组;
  3. 每个Entry都有k--v;
  4. Entry的key就是某个具体的ThreadLocal对象,是弱引用,key丢失后,GC不会回收Value会造成OOM(后面详细说)

 

3、ThreadLocal的set()方法

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);    //1
        if (map != null)
            map.set(this, value);  //2
        else
            createMap(t, value);
    }

可看出:

1、一个Thread只拥有一个ThreadLocalMap对象;

2、具体存值调用的是ThreadLocalMap的set(),传入的参数key就是当前ThreadLocal对象。

3.1、ThreadLocalMap的set()方法

private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1); // 1

            for (Entry e = tab[i];  // 2
                 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;
                }
            }

            tab[i] = new Entry(key, value); // 3
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold) // 4
                rehash();
        }

可看出:

1、通过key的hashCode与数组容量 -1 取模,计算数组index;

2、从当前index开始遍历,清除key为null的无效Entry

3、将K-V封装为Entry,并放入数组

4、判断是否需要进行Entry数组扩容。threshold的值为数组容量的2/3。

大家可以回顾一下HashMap的源码,是不是很相似?

index都是和长度-1取模,也都是容量不够了,超出阈值就扩容(这里是扩容为2倍,和HashMap一样)。

3.2、扩容方法resize()

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);  //1.计算新的索引
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }

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

1、然后遍历旧数组,根据新数组容量重新计算Entry在新数组中的位置。

 

4、ThreadLocal的get()方法

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t); 
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);   //ThreadLocalMap的getEntry()方法
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

4.1、ThreadLocalMap的getEntry()方法

private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1); // 1:计算索引
            Entry e = table[i];
            if (e != null && e.get() == key) // 2:当前Entry不为空,且key相同,直接返回entry;
                return e;
            else
                return getEntryAfterMiss(key, i, e); //3:否则去邻居index寻找
        }

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

            while (e != null) { //4:循环查找,发现无效key就清楚,直到找到结束循环;
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

 

5、ThreadLocal的remove()方法

public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);  //1:求得索引
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();    //清除entry<k,v>,显示调用,可预防OOM
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

 

注意:

static class Entry extends WeakReference<ThreadLocal<?>> {}

ThreadLocal可能存在OOM问题。

因为ThreadLocalMap是使用ThreadLocal的弱引用作为key的,发生GC时,key被回收,这样我们就无法访问key为null的value元素。

如果value本身是较大的对象,那么线程一直不结束的话,value就一直无法得到回收。特别是在我们使用线程池时,线程是复用的,不会杀死线程,这样ThreadLocal弱引用被回收时,value不会被回收。

所以,在使用ThreadLocal时,线程逻辑代码结束时,必须显示调用ThreadLocal.remove()方法。

 

 

参考致谢:

1、线程局部变量ThreadLocal实现原理

 

 

Over.......

posted @ 2021-02-16 17:57  额是无名小卒儿  阅读(237)  评论(0编辑  收藏  举报