ThreadLocal的那些事

1.什么是ThreadLocal

ThreadLocal是一种线程的变量副本,给每个调用的线程提供单独的变量属性,各个线程间的副互不干扰。

对于线程内的局部变量我们一般会怎么定义?我们一般会在线程内建立一个变量的声明,然后通过构造方法或者set方法把需要的变量值传进去,ThreadLocal方法则帮我们处理了这一过程,设置局部变量的操作可以在线程外执行,设置用法可以认为和我们常用的set的实现效果差不多,注意对于对象的设置因为指向一个内存地址,所以并不适合设置同一个,需要的话必须给每个线程设置新的对象

2.ThreadLocal的基本使用

   private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    private static ThreadLocal<Test> threadLocal2 = new ThreadLocal<>();
    private static Test test = new Test();
    private static class Test{
        private String name;
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        threadLocal.set("mainLocal");
        threadLocal2.set(test);
        Thread myThread1 = new MyThread("1");
        Thread myThread2 = new MyThread("2");
        myThread1.start();

        Thread.sleep(2000);

        myThread2.start();
        System.out.println("Thread main : ThreadLocal value ->>> "+threadLocal.get());
        System.out.println("Thread main : ThreadLocal value2 ->>> "+threadLocal2.get().getName());
    }

    private static class MyThread extends Thread {
        private String name;
        public MyThread(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            if(name.equals("1")){
                threadLocal.set("thread1 local");
                threadLocal2.set(test);
                threadLocal2.get().setName("重新设置名字");
            }else {
                threadLocal.set("thread2 local");
                threadLocal2.set(test);
            }

            System.out.println("Thread - " +name + " : ThreadLocal value ->>> "+ threadLocal.get());
            System.out.println("Thread - " +name + " : ThreadLocal value2 ->>> "+ threadLocal2.get().getName());

        }
    }

这里定义了两个ThreadLocal,一个是基本数据类型,另一个是对象类型;分别在相应的线程设置ThreadLocal值,并在最先执行的Thread1线程中通过ThreadLocal.get()方法获取局部变量并修改了该属性,打印结果是

Thread - 1 : ThreadLocal value ->>> thread1 local
Thread - 1 : ThreadLocal value2 ->>> 重新设置名字
Thread main : ThreadLocal value ->>> mainLocal
Thread main : ThreadLocal value2 ->>> 重新设置名字
Thread - 2 : ThreadLocal value ->>> thread2 local
Thread - 2 : ThreadLocal value2 ->>> 重新设置名字

可以看出,对于基本数据类型,每个线程持有的是不同的,这里主线程最先设置了ThreadLocal1的值,然后Thread1修改了这个ThreadLocal的值,而后续主线程打印仍然是最初的那个值

而变量则不同,因为设置的是同一个变量,那么后续只要有一处修改,那么所有线程获取的都是修改之后的值,这和我们常用的set对象变量的效果类似,因为对象在堆中分配了内存地址,所有的修改都指向同一片内存地址的修改,而取值也是从同一个内存地址中取值,所以是一样的,所以我们要避免这种误区,ThreadLocal不适合设置同一对象,这就失去了变量副本的功能,对于对象类型,每个线程也要保证唯一性,也就是每个线程持有的对象应该是不同的

3.ThreadLocal的实现原理

ThreadLocal我们常用的也就是set和get两种方法,分别查看这两种方法即可,先看下set方法的使用

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

可以看出,set方法会获取到调用该方法的线程,并获取该线程的ThreadLocalMap,从map中取出相应的值,而map的key就是当前的ThreadLocal本身,也就是说这个map是以ThreadLocal作为Key,具体的值为Value的集合

ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
 void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

ThreadLocalMap是Thread中的成员变量,set方法调用的时候会去取这个变量,如果没有初始化,那么就会调用createMap创建一个新的ThreadLocalMap,并初始化第一个键值对,把set的值传进去,并绑定当前的ThreadLocal

 static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    private static final int INITIAL_CAPACITY = 16;
    private Entry[] table;     
         
	 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

首先看一下构造方法,这里会初始化一个Entry的数组,默认初始大小长度是16
Entry是一个弱引用的子类,所关联的弱引用对象是ThreadLocal,并在构造方法中传入了我们所要存放的变量副本
然后通过位运算进行hash的取余操作计算出所存放的第一个的索引位置,并把当前的大小长度设置成1,并初始化负载因子,具体值为 16*2/3 = 10

   private final int threadLocalHashCode = nextHashCode();
   private static AtomicInteger nextHashCode =
        new AtomicInteger();
    private static final int HASH_INCREMENT = 0x61c88647;
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    
	private int threshold; // Default to 0
    private void setThreshold(int len) {
        threshold = len * 2 / 3;
    }

位运算的threadLocalHashCode转成十进制是一个黄金分割数1640531527,是保证hashcode均匀分布的最佳实践,几乎没有冲突问题,比我们的自增+1索引要好的多,具体原因好像涉及到斐波那契数列,没有研究过,有时间可以看一下
注意这里的nextHashCode是静态定义的,每次调用nextHashCode()方法都会进行自增一次,而threadLocalHashCode是非静态的,每次初始化都会调用nextHashCode()方法。也就是说每次新建的ThreadLocal的threadLocalHashCode都是不同的

 private void set(ThreadLocal<?> key, Object value) {
            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;
                }
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

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

set方法会先拿到当前的Entry的数组,根据存入key也就是ThreadLocal计算出相应的索引位置
然后从这个索引位置开始往后查询,如果本次所匹配的索引位置的Key不为空,那么就把本次的值存放到这个位置
如果本索引位置的Key是空的,那么就会调用replaceStaleEntry方法,传入本次的ThreadLocal和相应的要设置的值和本次的索引。注意这里的为空指定是Key为空而不是Entry为空,因为Entry是一个弱引用,所以这里的Key为空即表示,ThreadLocal被gc回收掉了
当遍历数组没有找到匹配的Key,同时也没有被回收的ThreadLocal,那么就会创建一个新的Entry,把本次的值传进去,并将数组大小自增1,然后校验此时的数组大小和threshold的值进行比较,决定是否进行rehash()

 private static int nextIndex(int i, int len) {
   return ((i + 1 < len) ? i + 1 : 0);

 private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
 }

nextIndexprevIndex很简单,就是往后和往前索引位偏移

 private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;
            int slotToExpunge = staleSlot;
            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;
                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);
             cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

Expunge英文释义为清除
这里会先把要清除的位置slotToExpunge定义为传入的索引,即查询出的第一个为空的Key的位置
然后先向前查询,如果前面有Key为null的位置,那么会把此次的slotToExpunge修正为最前的索引位置
然后向后查询,如果此次查询的的Entry中的Key和传入的Key相等,那么就修改此索引位置的Entry的值,把传入的Key为空的那个索引位置的Entry放到此位置,然后把修改后的这个Entry放到传入的索引位置,即上一个方法查出的第一个Key为空的位置;简单的说,就是把真正ThreadLocal这个Key不为空的索引位置往前移动,Key为空的往后移动。
判断slotToExpunge == staleSlot,也就是往前查询的是否有ThreadLocal的Key为空的情况,如果没有那么就把此次的索引给赋值上
最后如果没有找到Key为空的,也没有找到和传入的ThreadLocal匹配的,那么就把这个传入的这个索引位置的vaue置空,并重新建一个Entry存放信息
无论是否找到匹配的值,都会执行cleanSomeSlots的方法,传入的都是数组中Entry的Key为空的最新索引位置,因为有数组内元素替换的操作,这个索引会更新,然后通过方法expungStaleEnrty计算出要清除的位置

 private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;
            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;
                    }
                }
            }
            return i;
        }

这个方法是一个校验并更新的方法
这里会首先把本次传入的位置的值置空,并把size-1;然后从最初的位置再查询一遍,如果中间发现还有别的ThreadLocal为空的情况,那么会再次置空,并将size值继续降低,返回的是最后一个被置空的元素索引位置

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

这个主要是判断是否有元素被移除或置空的情况,如果有则返回ture
然后回到上面的ThreadlLocal的set方法,最后有一个判断代码

 if (!cleanSomeSlots(i, sz) && sz >= threshold)
      rehash();

当没有元素被清除的时候,同时数组里有实际值的大小长度大于threadhold时会进行rehash,也就是首次大于10的时候会执行

 private void rehash() {
            expungeStaleEntries();

            // Use lower threshold for doubling to avoid hysteresis
            if (size >= threshold - threshold / 4)
                resize();
        }
        
 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);
            }
        }  
private void resize() {
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            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();
                    if (k == null) {
                        e.value = null; // Help the GC
                    } else {
                        int h = k.threadLocalHashCode & (newLen - 1);
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }
            setThreshold(newLen);
            size = count;
            table = newTab;
        }      

可以看出,rehash前会再进行一轮校验,移除Entry中ThreadLocal为空的元素,重新计算真实长度
当长度大于threshold - threshold / 4时候会进行扩充,扩充大小是2倍,同时threshold也会更新为之前的2倍大小;
那么久可以看出
当数组中的元素的真实长度大于等于数组长度的2/3时,会进行扩容的判断,如果中间并没有元素被移除或者说没有发生ThreadLocal被回收的情况,就会进行下一步的扩容判断;当此时的数组内元素的真实长度大于等于数组长度的1/2时就会进行扩容,扩容的大小是1倍,就会变成原来数组长度的2倍

这个就是存放的逻辑和扩容的操作了
然后是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();
    }
    
 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);
        }
 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的方法比较简单,就是从已经存放的Map中去取相应的值,同样,这里的所有操作也都会进行ThreadLocal的弱引用判空的处理,这里就不再赘述了

4.总结

1.ThreadLocal的默认初始长度是16,默认的初始threshold16 *2 /3 = 10
2.ThreadLocal中定义有存放ThreadLocal和具体对象的ThreadLocalMap,而Thread中持有一个ThreadLocal的成员变量threadLocals,通过ThreadLocal的set方法会对该成员变量进行初始化并赋一个初始值。ThreadLocal的set方法在哪个线程调用的,就会为那个线程的threadLocals进行赋值初始化,通过Thread.currentThread()方式处理
3.ThreadLocalMap中维护了一个Entry数组,而Entry是一个弱引用,弱引用关联的是ThreadLocal对象,并在Enrty中定义了一个Object的成员变量,可以存放我们自定义的对象
4.当存放的元素大小大于等于threshold,也就是数组长度的2/3值时,同时数组元素中没有移除或置空的操作处理时候,会进行rehash的判断,而rehash中会进行resize的判断;当满足刚刚的那个条件后,仍然满足存的元素大小大于等于数组长度的1/2时,会进行数组的扩容,扩充到原来的2倍大小。这里的判断条件缩减了,从2/3缩减到了1/2,主要应该是考虑到gc对Entry中弱引用的回收,具体原因不清楚
5.添加和获取操作都会进行一系列的ThrealdLocal的Key的判空操作,并同步刷新数组信息

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值