深入学习ThreadLocal

目录

1.ThreadLoca的作用

2.ThreadLocal源码

get方法

set方法

扩容机制

Hash冲突时

3.ThreadLocal面试常问

ThreadLocal怎么解决hash冲突的?

ThreadLocal怎么解决内存泄漏的?

为什么ThreadLocal的key设计成弱引用,而Value不设计成弱引用?


1.ThreadLoca的作用

ThreadLoca的作用:可以实现线程隔离

简单案例了解一下ThreadLoca

public class ThreadLocalDemo {
    public static ThreadLocal<Integer> local = new ThreadLocal<Integer>(){
        //初始化值
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public static void main(String[] args) {
        Thread[] threads = new Thread[5];
        for (int i = 0; i < 5; i++) {
            threads[i] = new Thread(()->{
               int num = local.get();
               num += 5;
               local.set(num);
                System.out.println(Thread.currentThread().getName()+"num:" + local.get());
            });
        }

        for (int i = 0; i < 5; i++) {
            threads[i].start();
        }
    }
}

2.ThreadLocal源码

get方法

1.入口

public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //去根据Thread拿ThreadLocalMap,
        //1.1 我们发现Thread类下会有个数据 对象叫做ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //线程第一次进来,map肯定是null
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //1.2进入初始化方法
        return setInitialValue();
    }
1.1 获取线程中的 ThreadLocalMap 对象
static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        //..........其他信息省略..................
}
我们发现, Entry key 是一个对于 ThreadLocal 这个对象的弱引用。

Java四大引用

我们知道了 Entry key 是弱引用,弱引用的作用是什么我们知道了,那么 至于为什么要用弱引用,我们等下再回来看,先把整个流程搞清楚!

1.2 初始化方法

private T setInitialValue() {
        //根据用户定义的initialValue方法,去拿到我们的初始值
        T value = initialValue();
        //获取当前线程
        Thread t = Thread.currentThread();
        //去线程里面拿threadLocal,还是空的,进入createMap
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            //走到创建Map逻辑 1.2.1
            createMap(t, value);
        return value;
    }
1.2.1 初始化创建 Map
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
进入 ThreadLocalMap 初始化构造函数
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            //初始化Entry数组
            table = new Entry[INITIAL_CAPACITY];
            //根据线程的hashcode取模得到我应该放入数据的哪个下标位置
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            //在计算出的下标位置,放入entry,key为ThreadLocal对象,value 为初始化的值
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            //设置ThreadLocalMap的threshold值,16*2/3=10
            setThreshold(INITIAL_CAPACITY);
        }
自此,初始化流程完成!!

set方法

1.set入口

public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取线程的Map,这个时候,我们上面get的时候已经初始化,已经有值
        ThreadLocalMap map = getMap(t);
        //get如果在map之前执行,肯定不为null
        if (map != null)
            //进入set方法 1.1
            map.set(this, value);
        else
            createMap(t, value);
    }
1.1 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.

            //将原有的table赋值给tab
            Entry[] tab = table;
            //得到tabl的大小
            int len = tab.length;
            //根据key的hashCode 取模数组,得到数据的下标
            //同一个key的时候,hashcode一样,所以 
            //根据key找到的下标已经有entry对象并且已经赋值了初始化的值
            int i = key.threadLocalHashCode & (len-1);
            //在同一个key的get.set之后,e不为null,进入for循环
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                //得到entry的key
                ThreadLocal<?> k = e.get();
                //因为get set传入的threadlocal对象是一个,满足条件
                if (k == key) {
                    //将entry对象的value更改为新的value,返回
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            //没有进行清理并且size大于等于我的扩容界限,调用rehash
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
这个是最基础的流程,我们大概可以整理流程图如下:
假如2 个线程,操作 integerThreadLocal 这个 ThreadLocal 对象,并且ThreadLocal对象的 hash 值计算后在 Entry 中的数组下标为 5 ,integerThreadLocal下标为 3

正常流程结构如下 

 多个线程,就是多个外面的Thread,做到线程之间数据隔离

扩容机制

扩容机制 方法入口在set方法里

    //没有进行清理并且size大于等于我的扩容界限,调用rehash
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();

Rehash方法

private void rehash() { 
    expungeStaleEntries(); 
    // Use lower threshold for doubling to avoid hysteresis 
    //当容量大于等于四分之三时,进入resize方法 
    if (size >= threshold - threshold / 4) 
        resize(); 
}
resize方法
    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) {
                //遍历Enrty
                Entry e = oldTab[j];
                //如果Entry不为null
                if (e != null) {
                    //获取Entry的key
                    ThreadLocal<?> k = e.get();
                    //如果key为null,无效数据,把value设置为空,让value能 gc回收
                    if (k == null) {
                        e.value = null; // Help the GC
                    } else {
                        //不为空,得到k的新的下标地址
                        int h = k.threadLocalHashCode & (newLen - 1);
                        //如果!=null.代表发生hash冲突
                        while (newTab[h] != null)
                            //线性探测下一个
                            h = nextIndex(h, newLen);
                        //赋值给为空的entry位置
                        newTab[h] = e;
                        count++;
                    }
                }
            }
            //设置下一次的扩容值
            setThreshold(newLen);
            size = count;
            table = newTab;
        }

Hash冲突时

1. ThreadLocal ThreadLocal1 hash 值冲突
冲突存值
我们来看 set 方法中多线程中多个 ThreadLocal hashCode 冲突时,怎么解决,我们回到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;
            //这里可能发送hash冲突,假如threadLocal1跟threadLocal 
            //2个对 象的hash值相同,下标都是5
            int i = key.threadLocalHashCode & (len-1);
            //通过i去拿数据的Entry,我们拿到的是ThreadLocal的,
            //因为 ThreadLocal占据了5这个位置
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                //得到的是ThreadLocal对象
                ThreadLocal<?> k = e.get();
                //ThreadLocal !=ThreadLocal1
                if (k == key) {
                    e.value = value;
                    return;
                }
                //第一个循环 k也不等于null 
                //第二轮循环
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
                //3个条件都不满足,进入下一个循环 
                //i= nextIndex(i, len) 去找下一个小标的位置, 
                //直到找到下一个key为空的为止,这个场景我们等下过 
                //或者遍历完到一个null的位置,就不在循序
            }
            //找到一个为null的位置(肯定有,因为有扩容机制)
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
冲突取值
我们知道它是用的线性探测去解决 hash 的,那么会出现一个问题?我根据hash去拿到的对象,可能不再是我自己想要的对象
public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //根据threadLocal对象 去map中获取Entry,如果冲突了我们 看下怎么拿
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
getEntry方法
    private Entry getEntry(ThreadLocal<?> key) {
            //根据key的hash下标值去取值
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            //如果取到的enrty不为null 并且对象也是我需要的对象,直接返回
            if (e != null && e.get() == key)
                return e;
            else
                //如果不是我想要的对象,进入getEntryAfterMiss
                return getEntryAfterMiss(key, i, e);
        }
getEntryAfterMiss 方法
      //key:我需要get的对象 i 根据key计算出来的下标 e 下标中的当前值 
      private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;
            //如果e!=null。进入逻辑,如果e为null,
            //说明ThreadLocal没有设 置value,直接返回空
            while (e != null) {
                //得到当前位置的Entry对象
                ThreadLocal<?> k = e.get();
                //如果当前位置的Entry跟传进来的一致,直接返回
                if (k == key)
                    return e;
                if (k == null)
                    //如果对象的key被GC回收,进入整理逻辑,
                    //把当前位置设置为 null 并且进行整理,rehash
                    expungeStaleEntry(i);
                else
                    //去下一个线性找
                    i = nextIndex(i, len);
                //把e设置为下一个Enrty对象
                e = tab[i];
            }
            return null;
        }
2. Key GC 回收处理
        我们刚才讲过我们的key 是弱引用,何为弱引用,就是我这个 key 就算外面有引用,只要发生GC 也会被回收,就会出现我 Entry 的数据有可能是 key 为null ,但是 value 不为 null 的场景。
我们继续来看 ThreadLocal 怎么解决,继续回到 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;
                }
                //因为Key被回收,所以key为null,会进入replaceStaleEntry方 法
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //找到key为null的,不会走下面逻辑
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
replaceStaleEntry 方法
        看这个方法,我们举个例子:在Thread1 线程执行 threadLocal1.set10) ; 同时threadLocal1 通过 hash 算法得到的下标为 5 ;然后 5 的下标的 key GC 回收,key=null
//key为我需要获取值的ThreadLocal对象,value为需要set的值 i为key 被回收的数组下标 
//根据举例的场景:key为ThreadLocal1对象 value=10 i=5
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            //slotToExpunge为5
            int slotToExpunge = staleSlot;
            //向数组前面轮询找 找到一个null的entry为止 
            //假如下标为4的entry 为null,跳出循环
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                //假如下标为4的不是null,并且是被GC回收的,
                //那么 slotToExpunge赋值为向前找,找到最靠近null的被GC回收的Entry
                if (e.get() == null)
                    slotToExpunge = i;

            //向后循环,找到entry为null为止
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                //假如向后找到了key跟我传入的一样的entry            
                if (k == key) {
                    //如果一样,替换value
                    e.value = value;
                    //假如下标为7的跟我传入的key是一样的
                    tab[i] = tab[staleSlot];
                    //在下标为5的位置放入7下的entry
                    tab[staleSlot] = e;

                    //如果slotToExpunge=slotToExpunge,
                    //则向前遍历没有 找到key被回收的Entry
                    if (slotToExpunge == staleSlot)
                        //将slotToExpunge改成7
                        slotToExpunge = i;
                    //执行cleanSomeSlots方法
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    //返回
                    return;
                }

                //如果循环到后面的也是被GC回收的,并且向前遍历没有找到key被回收的Entry
                if (k == null && slotToExpunge == staleSlot)
                    //slotToExpunge设置为 key被GC回收的Entry的下标位置
                    slotToExpunge = i;
            }

            //回收的entry的value设置为null (利于value对象回收)
            tab[staleSlot].value = null;
            //在回收的下标位置,新建对象赋值
            tab[staleSlot] = new Entry(key, value);

            //slotToExpunge!=staleSlot,需要向前或者向后有找到需要清理的Entry,
            //执行cleanSomeSlots
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }
expungeStaleEntry方法
private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            //将传进来的下标位置的Entry value设置为null value就可以被GC 回收了
            //将传进来的下标位置的Entry设置为null 清理空间
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--; //数组里的size-1

            // Rehash until we encounter null
            Entry e;
            int i;
            //根据传进来的位置向后遍历,遍历到null为止
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                //如果entry对象的key被GC回收,清空entry
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    //如果entry的对象没有被GC回收
                    //重新去计算下这个位置下的key的hash
                    int h = k.threadLocalHashCode & (len - 1);
                    //如果占的位置不是它hash的位置
                    if (h != i) {
                        //把现在的位置设置为null
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
//看该有的位置是不是空的,如果不是,去找寻下一个 null的(开放寻址解决hash冲突)
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        //放到该有的位置去
                        tab[h] = e;
                    }
                }
            }
            //返回i的值 传进来的下标的 后面的最接近null的entry
            return i;
        }
cleanSomeSlots方法
    //i 传入下标 n为传进来的数组的长度
    private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                //根据传进来的下标去找下标后一个
                i = nextIndex(i, len);
                //得到该下标的enrty
                Entry e = tab[i];
                //如果下标的enrty 的key被GC回收了
                if (e != null && e.get() == null) {
                    //n改为table的长度
                    n = len;
                    removed = true;
                    //拿到i去清除与重新rehash后面的,直到找到null为止
                    i = expungeStaleEntry(i);
                }
            //不用遍历n次,只遍历n/2次, 达到时间与空间的平衡
            } while ( (n >>>= 1) != 0);
            return removed; //如果有清除,设置为true
        }
前面分析了 set 方法第一次初始化 ThreadLocalMap 的过程,也对ThreadLocalMap的结构有了一个全面的了解。那么接下来看一下 map 不为空时的执行逻辑
  • 根据key的散列哈希计算Entry的数组下标
  • 通过线性探索探测从i开始往后一直遍历到数组的最后一个Entry
  • 如果map中的key和传入的key相等,表示该数据已经存在,直接覆盖
  • 如果map中的key为空,则用新的keyvalue覆盖,并清理key=null的数据
  • rehash扩容

3.ThreadLocal面试常问

ThreadLocal怎么解决hash冲突的?

先回答一下ThreadLocal的数据结构

        每个线程里都会维护一个ThreadLocalMap对象所以能做到线程隔离;ThreadLocalMap里有entry对象是k,v结构,key是ThreadLocal对象,v是存储的value值,线程的ThreadLocal对象全部存在entry数组;会根据ThreadLocal对象的 hashcode(斐波那契算法,让散列更均匀,减少hash冲突),然后取模数组长度,得到下标值。但是hash值还是可能重复的,这样就会导致hash冲突 ;

如何解决:

写的时候,如果我发现我的位置被占有。那么就会循环去找下一个entry为 null 的位置,把该对象放入找到的位置.
查的时候,我也是去判断有没有冲突,如果有冲突,去向下找到为止。那么这个解决hash冲突的方法叫做线性探测。
     

ThreadLocal怎么解决内存泄漏的?

假如key value都是强引用的情况
因为threadlocal这个对象的生命周期其实是跟thread绑定的。那么如果key为强引用的话。
ThreadLocaL的对象一直会被线程的entry持有,然后ThreadLocaL对象回收不了,但是外面其实是可以把threadLocal对象强引用去掉的。那么这个时候,这个对象以及整个entry对象都是无效的,本来应该是需要回收的。但是因为强引用回收不了所以thread的key是弱引用,这样我无效的时候我能GC 回收对象。一定程度上面解决了内存泄漏问题。
但是不够
因为虽然key的对象能被回收了。但是entry对象还不能被回收。threadLocal通过各种机制,在使用的时候。会去回收key=null 的entry对象
  1. 当发生hash冲突的时候,并且冲突的这个位置的entry 的key被GC回收掉 了。 那么这个时候会从当前冲突的位置向前和向后分别找到离该节点最近的一个null节点,把这两个null节点范围内的所有key==null的entry节点让他们的value=null,然后再让整个entry节点=null从而让GC回收掉entry!!并且rehash
  2. cleanSomeSlots方法,会下标每次右移一位的方式遍历entry节点,然后把key=null的entry节点gc掉
  3. 2)扩容的时候,作数据迁移的时候会把key=null的节点,让他的value=null,从而帮助gc回收

还是不够

因为如果没有hash冲突,自身是不能走清除的。所以我们一般使用的时候,用完要remove.

为什么ThreadLocal的key设计成弱引用,而Value不设计成弱引用?

如果key是强引用的话,他就会被Entry对象一直持有,从而导致无法被gc回收掉,会造成内存泄漏,除非线程被回收掉,threadlocalmap才会跟着被回收掉。但是如果是弱引用的话,当执行threadLocal =null,的时候,key就会被回收掉,一定程度上防止了内存泄漏。

Value要是弱引用的话,可能会导致bug, 因为当value作为弱引用被回收掉时,value=null,但是这时key可能还存在,当我们getvalue的时候获取到的是一个null值,就可能会出现问题。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

w7486

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值