ThreadLocal的简介

多线程访问同一个共享变量时特别容易出现并发问题,特别是在多个线程需要对一个共享变量进行写入时,为了保证线程安全,一般使用者在访问共享变量时需要进行适当的同步,同步的措施一般就是加锁,加锁显然就是会阻塞其他线程,拉高了等待时间。
并发容器之ThreadLocal - 图1
ThreadLocalJDK包提供的,它提供了线程本地变量,也就是如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全的问题。
并发容器之ThreadLocal - 图2

ThreadLocal使用示例

实例开启了两个线程,在每个线程内部都设置了本地变量的值,然后调用print函数打印调用当前本地变量的值。如果打印后调用了本地变量的remove方法,则会删除本地的内存中的该变量、

  1. public class ThreadLocalTest {
  2. //(1)printf函数
  3. static void printf(String str){
  4. //1.1打印当前线程本地内存中localVariable变量的值
  5. System.out.println(str+":"+localVariable.get());
  6. //1.2清除当前线程本地内存中的localVariable变量的值
  7. localVariable.remove();
  8. }
  9. //(2)ThreadLocal变量
  10. static ThreadLocal<String> localVariable = new ThreadLocal<>();
  11. public static void main(String[] args) {
  12. //(3)创建线程one
  13. Thread threadone = new Thread(new Runnable() {
  14. @Override
  15. public void run() {
  16. localVariable.set("thread one local variable");
  17. //调用打印函数
  18. printf("Thread one");
  19. //打印本地变量值
  20. System.out.println("ThreadOne remove after"+":"+localVariable.get());
  21. }
  22. });
  23. //创建线程two
  24. Thread threadtwo = new Thread(new Runnable() {
  25. @Override
  26. public void run() {
  27. localVariable.set("thread two local variable");
  28. //调用打印函数
  29. printf("Thread two");
  30. //打印本地变量值
  31. System.out.println("ThreadTwo remove after"+":"+localVariable.get());
  32. }
  33. });
  34. //启动线程
  35. threadone.start();
  36. threadtwo.start();
  37. }
  38. }

image.png

线程one中的代码通过set方法设置了localVariable的值,这其实设置的是线程one本地内存的副本,这个副本Two是不能访问不了的,线程two的执行类似于线程One。

ThreadLocal的实现原理

ThreadLocal相关类的类图结构。
image.png

ThreadLocal的几个核心方法

Set方法

image.png
分析源码:
1.Thread t = thread.currentThread();//获取当前线程的实例对象
2.ThreadLocalMap map = getMap(t);//通过当前线程实例获取到ThreadLocalMap对象
3.if (map != null)// 如果Map不为null,则以当前threadLocl实例为key,值为value进行存入
map.set(this, value);
4. createMap(t, value);//4.map为null,则新建ThreadLocalMap并存入value
方法逻辑很清晰,通过源码我们知道value是存放在ThreadLocalMap里了。并且以threadLocal实例为key,接下来介绍ThreadLocalMap
image.png
该方法直接返回的就是当前线程对象t的一个成员变量threadLocals

image.png
也就是说ThreadLocalMap的引用作为Thread的一个成员变量,被Thread进行维护的,回头看set()方法,当map为Null的时候会通过createMap(t,value)方法、
image.png
该方法就是new一个ThreadLocalMap实例对象,然后同样以当前threadLocal实例作为key,值为value存放到threadLocalMap中,然后将当前线程对象的threadLocals赋值为threadLocalMap
image.png
现在对set()方法进行总结一下:通过当前线程对象thread获取该thread所维护的threadLocalMap,若threadLocalMap不为null,则以threadLocal实例为key,值为value的键值对存入threadLocalMap,若threadLocalMap为null的话,就新建threadLocalMap然后在以threadLocal为键,值为value的键值对存入即可

get方法
image.png

弄懂了set方法的逻辑,看get方法只需带着逆向思维去看就好了,如果是那样存的,反过来去拿就好,代码逻辑很清楚。另外需要看setInitialValue主要做了些什么?
image.png
这段代码和set的逻辑差不多,需要关注的是initialValue()

image.png
remove()方法
image.png
get,set()方法实现了存数据和读数据,我们当然还得学会如何删除数据。删除数据当然是从map中删除数据,
先获取当前线程相关联得threadLocalMap然后从map中删除该threadLocal实例为key的键值对即可

ThreadLocalMap详解

从上面的分析我们已经知道,数据其实都是放在了threadLocalMaop中,threadLocal的get,set和remove()方法实际上具体是通过threadLocalMap的getEntry,set和remove方法实现的。如果想全方位的弄懂threadLocal,势必得在对threadLocalMap做一番理解。

3.1 Entry数据结构

ThreadLocalMap是threadLocal一个静态内部类,和大多数容器一样维护了一个数组,同样的threadLocalMap维护了一个entry类型的table数组。
image.png
通过注释可以看出,table数组的长度为2的幂次方,接下来可以看看Entry是什么:

  1. static class Entry extends WeakRefence<ThreadLocal<?>>{
  2. Object value;
  3. Entry(ThreadLocal<?> k , object v){
  4. super(k);
  5. value = v;
  6. }
  7. }
  1. Entry是一个以Threadlocalkey,objectvalue的键值对,另外需要注意的是这里的threadLocal是**弱引用,因为Entry继承了WeakReference,在Entry的构造方法中,调用了super(k)方法就会将threadLocal实例包装成一个WeakReferece,**<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/22732206/1650093557887-66a78334-6f6a-457a-9d24-8c8957dd1d4e.png#clientId=u378d4e90-f4f2-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=343&id=u15effee3&margin=%5Bobject%20Object%5D&name=image.png&originHeight=425&originWidth=755&originalType=binary&ratio=1&rotation=0&showTitle=false&size=201061&status=done&style=none&taskId=uc062da17-af5e-4e10-aa94-31682a0c41e&title=&width=609.0756107274028)

内存泄露的问题

因为ThreadlocalMap和大多数容器一样维护了一个entry类型的table数组,Entry是一个以ThreadLocalkey,Objectvalue的键值对,另外需要注意的是threadlocal的弱引用,因为Entry继承了WeakReference,在Entry的构造方法中,调用了super(k)方法就会将threadlocal实例包装成一个WeakReference,Entry中的key是弱引用,当threadlocal外部强引用被置为null,那么系统GC的时候,根据可达性原则,这个Threadlical实例就没有任何一条链路能够引用到它,这个ThreadLocal势必就会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,而value不为空,就会出现内存泄露的问题。
解决方法:用完threadlocal就立即调用remove方法,在其内部的get,set,remove,它方法体也隐式清除了

3.2 set方法

  1. private void set(ThreadLocal<?> key, Object value) {
  2. // We don't use a fast path as with get() because it is at
  3. // least as common to use set() to create new entries as
  4. // it is to replace existing ones, in which case, a fast
  5. // path would fail more often than not.
  6. Entry[] tab = table;
  7. int len = tab.length;
  8. //根据threadLocal的hashCode确定Entry应该存放的位置
  9. int i = key.threadLocalHashCode & (len-1);
  10. //采用开放地址法,hash冲突的时候使用线性探测
  11. for (Entry e = tab[i];
  12. e != null;
  13. e = tab[i = nextIndex(i, len)]) {
  14. ThreadLocal<?> k = e.get();
  15. //覆盖旧Entry
  16. if (k == key) {
  17. e.value = value;
  18. return;
  19. }
  20. //当key为null时,说明threadLocal强引用已经被释放掉,那么就无法
  21. //再通过这个key获取threadLocalMap中对应的entry,这里就存在内存泄漏的可能性
  22. if (k == null) {
  23. //用当前插入的值替换掉这个key为null的“脏”entry
  24. replaceStaleEntry(key, value, i);
  25. return;
  26. }
  27. }
  28. //新建entry并插入table中i处
  29. tab[i] = new Entry(key, value);
  30. int sz = ++size;
  31. //插入后再次清除一些key为null的“脏”entry,如果大于阈值就需要扩容
  32. if (!cleanSomeSlots(i, sz) && sz >= threshold)
  33. rehash();
  34. }

1.根据threadLocal的hascode确定Entry应该存放的位置
2.如果发现Entry已经存放了值了就利用开放地址法向后找为空的Entry,如果发现Entry的key或者为null为同一个key就覆盖掉。
3.插入后再次清除一些key为null的脏”entry”,如果大于阈值就进行扩容

threshold的确定

  1. private int threshold; // Default to 0
  2. /**
  3. * The initial capacity -- MUST be a power of two.
  4. */
  5. private static final int INITIAL_CAPACITY = 16;
  6. ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
  7. table = new Entry[INITIAL_CAPACITY];
  8. int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  9. table[i] = new Entry(firstKey, firstValue);
  10. size = 1;
  11. setThreshold(INITIAL_CAPACITY);
  12. }
  13. /**
  14. * Set the resize threshold to maintain at worst a 2/3 load factor.
  15. */
  16. private void setThreshold(int len) {
  17. threshold = len * 2 / 3;
  18. }

根据源码已知,在第一次为threadLocal进行赋值的时候会创建初始大小的thraedLocalMap,并且通过setThraedhold方法设置thresholde,其值为当前哈希数组长度乘以2/3

  1. /**
  2. * Double the capacity of the table.
  3. */
  4. private void resize() {
  5. Entry[] oldTab = table;
  6. int oldLen = oldTab.length;
  7. //新数组为原数组的2倍
  8. int newLen = oldLen * 2;
  9. Entry[] newTab = new Entry[newLen];
  10. int count = 0;
  11. for (int j = 0; j < oldLen; ++j) {
  12. Entry e = oldTab[j];
  13. if (e != null) {
  14. ThreadLocal<?> k = e.get();
  15. //遍历过程中如果遇到脏entry的话直接另value为null,有助于value能够被回收
  16. if (k == null) {
  17. e.value = null; // Help the GC
  18. } else {
  19. //重新确定entry在新数组的位置,然后进行插入
  20. int h = k.threadLocalHashCode & (newLen - 1);
  21. while (newTab[h] != null)
  22. h = nextIndex(h, newLen);
  23. newTab[h] = e;
  24. count++;
  25. }
  26. }
  27. }
  28. //设置新哈希表的threshHold和size属性
  29. setThreshold(newLen);
  30. size = count;
  31. table = newTab;
  32. }

方法逻辑请看注释,新建一个大小为原来数组长度的两倍的数组,然后遍历旧数组中的entry并将其插入到新的hash数组中,主要注意的是,在扩容的过程中针对脏entry的话会令value为null,以便能够被垃圾回收器能够回收,解决隐藏的内存泄漏的问题。

3.3 getEntry方法private Entry getEntry(ThreadLocal<?> key) {

  1. private Entry getEntry(ThreadLocal<?> key) {
  2. //1. 确定在散列数组中的位置
  3. int i = key.threadLocalHashCode & (table.length - 1);
  4. //2. 根据索引i获取entry
  5. Entry e = table[i];
  6. //3. 满足条件则返回该entry
  7. if (e != null && e.get() == key)
  8. return e;
  9. else
  10. //4. 未查找到满足条件的entry,额外在做的处理
  11. return getEntryAfterMiss(key, i, e);
  12. }

方法逻辑很简单,若能当前定位的entry的key和查找的key相同的话就直接返回这个entry,否则的话就是在set的时候存在hash冲突的情况,需要通过getEntryAfterMiss做进一步处理。getEntryAfterMiss方法为:

  1. private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
  2. Entry[] tab = table;
  3. int len = tab.length;
  4. while (e != null) {
  5. ThreadLocal<?> k = e.get();
  6. if (k == key)
  7. //找到和查询的key相同的entry则返回
  8. return e;
  9. if (k == null)
  10. //解决脏entry的问题
  11. expungeStaleEntry(i);
  12. else
  13. //继续向后环形查找
  14. i = nextIndex(i, len);
  15. e = tab[i];
  16. }
  17. return null;
  18. }

这个方法同样很好理解,通过nextIndex往后环形查找,如果找到和查询的key相同的entry的话就直接返回,如果在查找过程中遇到脏entry的话使用expungeStaleEntry方法进行处理。到目前为止,为了解决潜在的内存泄漏的问题,在set,resize,getEntry这些地方都会对这些脏entry进行处理,可见为了尽可能解决这个问题几乎无时无刻都在做出努力。


3.4 remove

  1. /**
  2. * Remove the entry for key.
  3. */
  4. private void remove(ThreadLocal<?> key) {
  5. Entry[] tab = table;
  6. int len = tab.length;
  7. int i = key.threadLocalHashCode & (len-1);
  8. for (Entry e = tab[i];
  9. e != null;
  10. e = tab[i = nextIndex(i, len)]) {
  11. if (e.get() == key) {
  12. //将entry的key置为null
  13. e.clear();
  14. //将该entry的value也置为null
  15. expungeStaleEntry(i);
  16. return;
  17. }
  18. }
  19. }

该方法逻辑很简单,通过往后环形查找到与指定key相同的entry后,先通过clear方法将key置为null后,使其转换为一个脏entry,然后调用expungeStaleEntry方法将其value置为null,以便垃圾回收时能够清理,同时将table[i]置为null。