深度解析ThreadLocal ? Key是当前线程?

1,792 阅读3分钟

ThreadLocal作为多线程中必备的一个神奇,保证数据安全,将线程的变量进行隔离,有非常多的使用场景.

例如:

假如我们商城下单的时候,我们需要用到多线程处理订单,就可以把提交的订单信息保存到ThreadLocal中,让每个线程处理自己的内容,线程之间没有各种麻烦的抢夺,数据资源不一致的问题。如下图,用户A,用户B 提交的数据互不干扰 image.png

很简单的demo,不做过多阐述

private ThreadLocal<Order> threadLocal = new ThreadLocal<>();
    public void post(Order order) {
        try {
            threadLocal.set(order);
            doOrder();
        } finally {
            threadLocal.remove();
        }
    }
    private void doOrder() {
        System.out.println("处理订单逻辑");
    }

重点1,ThreadLocal 是怎么做到线程变量之间的隔离的? 上图

image.png

解释图形

  • 首先通过ThreadLocal<Order> threadLocal = new ThreadLocal<>();初始化了一个ThreadLocal对象,就是上图中 ThreadLocal引用了 ThreadLocal对象

  • 调用 threadLocal.set(order); 具体操作 :

//线程局部变量的当前线程副本设置为指定值。 大多数子类将不需要重写此方法,而仅依靠initialValue方法来设置线程initialValue的值。
public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t); --> t.threadLocals
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value); -->t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

public class Thread implements Runnable {
     ThreadLocal.ThreadLocalMap threadLocals = null;
}

Thread 类有个 ThreadLocalMap 成员变量,这个ThreadLocalMap中的Key是Threadlocal 对象,value是要存放的线程局部变量。 set 也就是向当前线程的ThreadLocalMap中存放了一个元素(Entry),Key是ThreadLocal对象,value就是order

  • ThreadLocalMap 在Thread中
  1. Thread 对象是Java语言中线程运行的载体,每个线程都有对应的Thread 对象,存放线程相关的一些信息

  2. Thread类中有个成员变量ThreadlocalMap,普通的Map,key存放的是Threadlocal对象,value是你要跟线程绑定的值(线程隔离的变量),比如这里是用户信息对象(order).

为什么ThreadLcoalMap要定义在Thread中

  ThreaLocalMap是自定义的哈希映射,仅适用于维护线程局部值。 没有操作导出到ThreadLocal类之外。 该类是包私有的,以允许声明Thread类中的字段。 为了帮助处理非常长的使用寿命,哈希表条目使用WeakReferences作为键。 但是,由于不使用参考队列,因此仅在表空间不足时,才保证删除过时的条目。

为什么不用Thread当作KEY ?取数据不是更加方便吗?

不可以,因为如果现在是只有一个Order对象操作,如果在家一个优惠券信息,那该怎么办?如果重新set不是就把原先的内容给覆盖了吗。简直是有点扯淡,

如果在ThreaLocalMap中重新添加一个第二个元素,

只需要 重新创建一个 private ThreadLocal<CardInfo> cardInfoThreadLocal = new ThreadLocal<>(); 就可以了,这样当前线程就会有优惠券信息了

ThreadlocalMap 是什么数据结构实现的?

class ThreadLocalMap {
 //初始容量
 private static final int INITIAL_CAPACITY = 16;
 //存放元素的数组
 private Entry[] table;
 //元素个数
 private int size = 0;
}

table 就是存储线程局部变量的数组,数组元素是Entry类,Entry由key和value组成,key是Threadlocal对象,value是存放的对应线程变量

image.png

ThreadLocalMap发生Hash冲突怎么解决?

ThreadLocalMap 采用的是开放定址法,如果发生冲突,就往后找相邻的下一个节点,如果相邻的节点是空的,那么久直接存进去,如果不为空,继续往后查找,如果找到数据的最后也没有找到空的,就扩容

private void set(ThreadLocal<?> key, Object value) {
  Entry[] tab = table;
  int len = tab.length;
  // hashcode & 操作其实就是 %数组长度取余数,例如:数组长度是4,hashCode % (4-1) 就找到要存放元素的数组下标
  int i = key.threadLocalHashCode & (len-1);

  //找到数组的空槽(=null),一般ThreadlocalMap存放元素不会很多
  for (Entry e = tab[i];
       e != null; //找到数组的空槽(=null)
       e = tab[i = nextIndex(i, len)]) {
    ThreadLocal<?> k = e.get();

    //如果key值一样,算是更新操作,直接替换
    if (k == key) {
      e.value = value;
      return;
    }
  //key为空,做替换清理动作,这个后面聊WeakReference的时候讲
    if (k == null) {
      replaceStaleEntry(key, value, i);
      return;
    }
  }
 //新new一个Entry
  tab[i] = new Entry(key, value);
  //数组元素个数+1
  int sz = ++size;
  //如果没清理掉元素或者存放元素个数超过数组阈值,进行扩容
  if (!cleanSomeSlots(i, sz) && sz >= threshold)
    rehash();
}

//顺序遍历 +1 到了数组尾部,又回到数组头部(0这个位置)
private static int nextIndex(int i, int len) {
  return ((i + 1 < len) ? i + 1 : 0);
}

// get()方法,根据ThreadLocal key获取线程变量
private Entry getEntry(ThreadLocal<?> key) {
  //计算hash值 & 操作其实就是 %数组长度取余数,例如:数组长度是4,hashCode % (4-1) 就找到要查询的数组地址
  int i = key.threadLocalHashCode & (table.length - 1);
  Entry e = table[i];
  //快速判断 如果这个位置有值,key相等表示找到了,直接返回
  if (e != null && e.get() == key)
    return e;
  else
    return getEntryAfterMiss(key, i, e); //miss之后顺序往后找(链地址法,这个后面再介绍)
}

如果不想让很多变量里面塞到ThreadLocalMap中造成扩容,怎么办 ?

答案就是 : 把ThreadLcoal的Value设置为Map值就行了