ThreadLocal小白浅述原理,version 1.0

前言:ThreadLocal平常用的比较少,但是面试有被问到好多次、痛定思痛准备花几天自己好好看一下源码。小白看源码系列,以后有更深层次的研究,在将博客更新plus,我的技术在长大、我的博客亦然要长大。threallocal到底是个啥东东呢?下面一段话摘自源码

This class provides thread-local variables. These variables differ
from their normal counterparts in that each thread that accesses one
(via its get or set method) has its own, independently initialized
copy of the variable. ThreadLocal instances are typically private
static fields in classes that wish to associate state with a thread
(e.g., a user ID or Transaction ID).

中英互译如下:

此类提供线程局部变量。这些变量不同于它们的普通对应变量,因为访问一个变量的每个线程(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是希望将状态与线程(例如,用户ID或事务ID)关联的类中的私有静态字段。

简单来说threadlocal就是将属性与线程进行一一绑定 ,每个线程中的东东都已经长大分家了,都在过着自己的小日子,互相不干扰。这样做的好处就是,在多线程状态下,可以抗并发。

threadlocal爱之初体验

先看下面一段代码: 线程1往list中存数据、线程2从list中取出数据。显然这个数据是能取出来的。list就相当于一个容器,对同一个容器的存取操作必然成功。那如果把list换成threadlocal呢?

private static List<String> list = new ArrayList<>();
		//线程1
        new Thread(() -> {
    
    
            boolean value = list.add("value");
            if (value) {
    
    
                System.out.println("添加成功!");
            }
        }).start();
        //线程2
        new Thread(() -> {
    
    
            System.out.println(list.get(0));
        }).start();

同样线程1往threadlocal中存数据,线程2往threadlocal中取出数据。此时线程2并不能获取到数据,从这里就体现出来了threadlocal的线程隔离性

    private static ThreadLocal<user> threadLocal = new ThreadLocal<user>();
		//线程1
        new Thread(() -> {
    
    
            threadLocal.set(new user());
        }).start();
        //线程2
        new Thread(() -> {
    
    
            System.out.println(threadLocal.get());
        }).start();

threadlocal深入探究

为啥threadlocal能做到线程隔离呢?从set方法探究奥秘。本质就是:threadlocal底层是有一个threadlocalmap来维护的,而这个threadlocalmap是从当前线程中获取的,且这个map中的

  • key:this=threadlocal
  • value:value。
  public void set(T value) {
    
    
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

深入探究threadlocalmap是如何进行set的,本质看这一行代码, threadlocalmap是以Entry[]作为存储容器把key、value封装成一个Entry对象,放到Entry数组里面

tab[i] = new Entry(key, value);
private void set(ThreadLocal<?> key, Object value) {
    
    
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
//循环遍历entry数组
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
    
    
                ThreadLocal<?> k = e.get();
//是同一个threadlocal进行set那么将value覆盖
                if (k == key) {
    
    
                    e.value = value;
                    return;
                }
//如果k为null,说明此引用引用的对象已经被gc或者被程序清理掉了
//用新key、value覆盖,同时清理掉旧的value值
                if (k == null) {
    
    
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
//key、value封装成entry对象放入entry数组
            tab[i] = new Entry(key, value);
            int sz = ++size;
//大于阈值还有一个条件,需要进行扩容
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

接着细看, 散列算法:保证了数组下标(i)一定在0-table.length之间

int i = key.threadLocalHashCode & (len-1);

i++操作

nextIndex(i, len)

e.get()<=>tab[i].get()<=>Entry.get() 。参照下方代码理解

  • key: 指实参this,也就是set()的调用对象threadlocal.set() 中的threadlocal。也是thread中的threadlocal
  • value: value
private void set(ThreadLocal<?> key, Object value){
    
    
	tab[i] = new Entry(key, value);
	Entry e = tab[i];
	ThreadLocal<?> k = e.get();
}

get()方法源码注释
Returns this reference object’s referent. If this reference object has
been cleared, either by the program or by the garbage collector, then
this method returns null. Returns: The object to which this reference
refers, or null if this reference object has been cleared

返回此引用对象的referent。如果此引用对象已被程序或垃圾收集器清除,则此方法返回null。return: 此引用引用的对象,如果已清除此引用对象,则为null

看一下Entry的源码,得知Entry继承弱引用,Entry中的key(thread中的threadlocal)是弱引用,那么 e.get()的返回值就是tab[i]引用引用的对象,就是指Ehtry(key,value)的key值

这里有点绕哈,tab[i]是一个强引用指向e(Entry),e(Entry)是一个弱引用,这个弱引用指向Entry(key,value)中的key。

这里有一个思考问题,为什么Ehtry要使用弱引用呢?文章后面有分析到。涉及到内存泄漏的问题

static class Entry extends WeakReference<ThreadLocal<?>> {
    
    
            /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
    
    
                //super:调用父类构造方法,k就是弱引用
                super(k);
                value = v;
            }
        }

小结

  1. threadlocal里面有一个threadlocalmap来维护数据,threadlocalmap里面是一个entry数组来维护数据关系。
  2. e.get()的返回值是引用引用的对象,是指Entry中的虚引用。如果此引用已经被gc那么返回null
  3. k=key表明是同一个threadlocal的使用,进行value的覆盖
  4. key==null,说明此threadlocal已经被gc或者回收清理
  5. 阈值为 2/3*len
  6. 确定下标散列算法:threadlocal.threadLocalHashCode&(len-1)
  7. entry体现弱引用的地方,super(k)
  8. threadlocal在进行set的时候把自己也传进去了,也就是那个this
  9. 扩容的条件:!cleanSomeSlots(i, sz) && sz >= threshold
  10. threadlocalmap是thread中的一个属性,每个线程中都有这个东西
  11. 弱引用:一旦进行gc那么引用就没了

接下来的replaceStaleEntry()方法、rehash()方法有点难啃,请读者做好心理准备,❤️就是好绕啊啊啊啊啊啊,人都快被搞吐血了我。

rehash方法

expungeStaleEntries: 扩容之前会先清除旧的Ehtry

private void rehash() {
    
    
            expungeStaleEntries();

            // Use lower threshold for doubling to avoid hysteresis
            if (size >= threshold - threshold / 4)
                resize();
        }

遍历tab表,如果遍历到已经被GC的节点,那么清除此entry

private void expungeStaleEntries() {
    
    
            Entry[] tab = table;
            int len = tab.length;
            for (int j = 0; j < len; j++) {
    
    
                Entry e = tab[j];
                if (e != null && e.get() == null)
                    expungeStaleEntry(j);
            }
        }

expungeStaleEntry方法做了俩件事情

  1. 扫描整个table表,清除旧的指定下标的无效Entry
  2. 清除指定下标右边无效的Entry
private int expungeStaleEntry(int staleSlot) {
    
    
            Entry[] tab = table;
            int len = tab.length;
//删除旧的指定位置的entry
            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;
//重新hash计算
            // Rehash until we encounter null
            Entry e;
            int i;
//接着staleSlot的位置开始遍历tab表
            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;

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

思考问题:

  • 清除无效的entry直接tab[staleslot] = null就好了,为什么要多一步tab[staleslot].value = null操作呢?

到此我们来撸一下threadlocal中的关系,如下图
在这里插入图片描述
结合图片与代码阅读更佳 当我们在调用如下代码的时候,会有一个强引用指向new ThreadLocal()对象,同时Entry中还有一个弱引用指向new ThreadLocal()对象,当程序执行完毕时进行销毁ThreadLocal对象的时候,也就是设置threadLocal = null时候,如果Entry设置成强引用,那么即使设置threadLocal = null,Entry中的这个引用也还是消除不了的,会导致new ThreadLocal()这个对象永远没法被回收,但是设置成弱引用的话,强引用删除了,弱引用自然也跟着没有了,new ThreadLocal()就可以被回收了。

ThreadLocal<user> threadLocal = new ThreadLocal<user>();
threadlocal.set(new user);
threadlocal.get();

小结 :为什么Ehtry要使用弱引用呢?
简答:强引用的删除、连带弱引用也删除,从而得以让ThreadLocal对象能被及时回收

清除旧的entry直接tab[staleslot] = null就好了,为什么之前要tab[staleslot].value = null 呢?
回答:我们tab[i] = null 的目的就是让Entry能被及时回收,但是Entry对象是不是只有key才是弱引用啊!只进行tab[i] = null而不进行删除 value操作,将会导致key=null、而value由于没有指针来指向它了,value会一直删除不掉,从而导致Entry对象永远回收不了,内存会卡卡的往上面飙,这就是内存泄漏呀,所以需要设置value = null,让Entry对象及时回收(GC)

//源码中的这行代码复制进我们新建的一个类中
tab[i] = new Entry(key, value);
//得到这样子的代码
tab[i] = new ThreadLocal().ThreadLocalMap.Entry(threadLocal, value);

threadlocal内存泄露问题解决: 值得注意的是threadlocal在进行回收的时候,还是存在内存泄露问题的。我们只有在调用get、set方法的时候,才会把key=null的Entry对象的value = null,所以我们在使用完ThreadLocal对象的时候,最好remove一下,避免value值一直存在而导致的Entry无法回收的问题。

到这里为止,所有无效的Entry就已经被消除了,接下来就可以进行扩容了。

resize

  • 扩容的条件: size >=threshold*3/4
  • 新容量大小: newlen = oldLen * 2
  • 设置threshold= newlen
  • setThreshold(newLen);
  • size = count;
  • table = newTab;

没啥好看的,清除无效Entry,剩下有效的Entry重新计算hash值下标,放入新tab[]

private void resize() {
    
    
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            //2倍容量
            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();
                    //帮助Entry GC,
                    if (k == null) {
    
    
                        e.value = null; // Help the GC
                    } else {
    
    
                    //重新hash计算下标
                        int h = k.threadLocalHashCode & (newLen - 1);
                        //在新tab[]中找空位
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                            //放入新tab[]中
                        newTab[h] = e;
                        //size++
                        count++;
                    }
                }
            }
            //设置阈值
            setThreshold(newLen);
            //设置
            size = count;
            table = newTab;
        }

replaceStaleEntry TODO

研究的不深!主要作用:用当前值替换已经失效的值

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
    
    
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            // Back up to check for prior stale entry in current run.
            // We clean out whole runs at a time to avoid continual
            // incremental rehashing due to garbage collector freeing
            // up refs in bunches (i.e., whenever the collector runs).
            int slotToExpunge = staleSlot;
            //往前遍历找到最早的无效Entry下标
            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();

                // If we find key, then we need to swap it
                // with the stale entry to maintain hash table order.
                // The newly stale slot, or any other stale slot
                // encountered above it, can then be sent to expungeStaleEntry
                // to remove or rehash all of the other entries in run.
                if (k == key) {
    
    
                    e.value = value;

                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // Start expunge at preceding stale entry if it exists
                    //尽量清除无效的Entry对象
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                // If we didn't find stale entry on backward scan, the
                // first stale entry seen while scanning for key is the
                // first still present in the run.
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // If key not found, put new entry in stale slot
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // If there are any other stale entries in run, expunge them
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

以下为get部分

就是从ThreadLocalMap中获取Entry对象,继而获取里面的value值。可以研究一下getEntry(this)方法

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

getEntry()

俩中情况:

  1. e!=null && this == e.get(),直接返回此Entry
  2. 其他情况进行全表扫描,查找Entry

本质就是利用key计算出下标,然后从Entry[]中找到对应的Entry对象。

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

介绍:在其直接哈希槽中找不到键时使用的getEntry方法的版本。

  1. e == null 情况下return null;
  2. e!=null && e.get() != this 导致这种情况的出现有,此Entry已经换了位置,继而从getEntryAfterMiss()中再来一次全tab扫描,看是否存在此Entry,实在找不到return null;

getEntryAfterMiss()这里面体现出了,设置Entry.value=null帮助Entry对象回收的过程。

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

get总结:如果从tab[]中找到的Entry对象满足条件,那么直接返回,否则遍历tab[]寻找符合条件的Entry,同时清理无效的Entry对象,实在找不到Return null;

remove方法

ThreadLocal对象不用了,切记要remove,不然该内存泄漏了

介绍:清除ThradLocal对象,同时清理无效的Entry对象

 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);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
    
    
                if (e.get() == key) {
    
    
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

文章大总结

小结 :为什么Ehtry要使用弱引用呢?
简答:强引用的删除、连带弱引用也删除,从而得以让ThreadLocal对象能被及时回收

清除旧的entry直接tab[staleslot] = null就好了,为什么之前要tab[staleslot].value = null 呢?
回答:我们tab[i] = null 的目的就是让Entry能被及时回收,但是Entry对象是不是只有key才是弱引用啊!只进行tab[i] = null而不进行删除 value操作,将会导致key=null、而value由于没有指针来指向它了,value会一直删除不掉,从而导致Entry对象永远回收不了,内存会卡卡的往上面飙,这就是内存泄漏呀,所以需要设置value = null,让Entry对象及时回收(GC)

  • 扩容的条件: size >=threshold*3/4
  • 新容量大小: newlen = oldLen * 2
  • 设置threshold= newlen
  • setThreshold(newLen);
  • size = count;
  • table = newTab;

threadlocal内存泄露问题解决: 值得注意的是threadlocal在进行回收的时候,还是存在内存泄露问题的。我们只有在调用get、set方法的时候,才会把key=null的Entry对象的value = null,所以我们在使用完ThreadLocal对象的时候,最好remove一下,避免value值一直存在而导致的Entry无法回收的问题。

有一个强引用指向new ThreadLocal()对象,同时Entry中还有一个弱引用指向new ThreadLocal()对象,当程序执行完毕时进行销毁ThreadLocal对象的时候,也就是设置threadLocal = null时候,如果Entry设置成强引用,那么即使设置threadLocal = null,Entry中的这个引用也还是消除不了的,会导致new ThreadLocal()这个对象永远没法被回收,但是设置成弱引用的话,强引用删除了,弱引用自然也跟着没有了,new ThreadLocal()就可以被回收了。

  1. threadlocal里面有一个threadlocalmap来维护数据,threadlocalmap里面是一个entry数组来维护数据关系。
  2. e.get()的返回值是引用引用的对象,是指Entry中的虚引用。如果此引用已经被gc那么返回null
  3. k=key表明是同一个threadlocal的使用,进行value的覆盖
  4. key==null,说明此threadlocal已经被gc或者回收清理
  5. 阈值为 2/3*len
  6. 确定下标散列算法:threadlocal.threadLocalHashCode&(len-1)
  7. entry体现弱引用的地方,super(k)
  8. threadlocal在进行set的时候把自己也传进去了,也就是那个this
  9. 扩容的条件:!cleanSomeSlots(i, sz) && sz >= threshold
  10. threadlocalmap是thread中的一个属性,每个线程中都有这个东西
  11. 弱引用:一旦进行gc那么引用就没了

e.get()<=>tab[i].get()<=>Entry.get()

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_42875345/article/details/114496620
今日推荐