Threadlocal学习总结

作用&场景

作用:主要用来做线程变量的隔离。
场景:支付宝每秒钟同时会有很多用户请求,那每个请求都带有用户信息,我们知道通常都是一个线程处理一个用户请求,我们可以把用户信息丢到Threadlocal里面,让每个线程处理自己的用户信息,线程之间互不干扰。

程序在处理用户请求的时候,通常后端服务器是有一个线程池,来一个请求就交给一个线程来处理,那为了防止多线程并发处理请求的时候发生串数据,比如AB线程分别处理用户a和用户b的请求,A线程本来处理用户a的请求,结果访问到用户b的数据上了,把用户b支付宝的钱转走了。所以就可以把用户a的数据跟A线程绑定,线程处理完之后解除绑定。

原理图

伪代码如下:

//定义全局常量,多线程彼此之间也不会干扰。
private static final ThreadLocal<UserInfo> userInfoThreadLocal = new ThreadLocal<>();
public void handleRequest(UserInfo userInfo) {
  try {
    //用户信息set到线程局部变量中
    userInfoThreadLocal.set(userInfo);
    //业务处理
  } finally {
    //使用完移除掉
    userInfoThreadLocal.remove();
  }
}
//查看Threadlocal类
 public void set(T value) {
        Thread t = Thread.currentThread();///获取当前线程Thread,就是上图画的Thread引用。
        ThreadLocalMap map = getMap(t);Thread类有个成员变量ThreadlocalMap,拿到这个Map。
        if (map != null)
            map.set(this, value);//this指的就是Threadlocal对象。
        else
           createMap(t, value);//第一次创建ThreadLocalMap对象。
 }
 ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;获取线程的ThreadLocalMap
 }
//Thread类 Thread.class
public class Thread implements Runnable {
    //每个线程都有自己的ThreadLocalMap 成员变量。
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

ThreadLocalMap 类的定义在Threadlocal中。
Thread 对象是Java语言中线程运行的载体,每个线程都有对应的Thread 对象,存放线程相关的一些信息。
Thread类中有个成员变量ThreadlocalMap,key存放的是Threadlocal对象,value是你要跟线程绑定的值(线程隔离的变量),比如这里是用户信息对象(UserInfo)。
Thread 类有个ThreadlocalMap属性的成员变量,但是ThreadlocalMap的定义却在Threadlocal 中??

ThreadLocalMap is a customized hash map suitable only for maintaining thread local values. 
No operations are exported outside of the ThreadLocal class. 
The class is package private to allow declaration of fields in class Thread. 
To help deal with very large and long-lived usages, 
the hash table entries use WeakReferences for keys. However, 
since reference queues are not used,
stale entries are guaranteed to be removed only when the table starts running out of space.

大概意思是ThreadLocalMap 就是为维护线程本地变量而设计的,只做这一件事情。
ThreadlocalMap作为Threadlocal 的内部类,只有包访问权限,就是让使用者知道ThreadLocalMap就只做保存线程局部变量这一件事的。
既然是线程局部变量,为什么不用Thread对象做ThreadLocalMap的key??
因为这样就无法为这个线程保存多个线程私有的信息了。key一样,set信息时,后者覆盖前者。
前者使用userInfoThreadLocal保存用户信息,我们可以再创建一个ThreadLocal对象保存其他信息。
private static final Threadlocal other = new Threadlocal();
other可以保存其他信息。

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是存放的对应线程变量。Entry继承了弱引用WeakReference(后面会提到)。

哈希冲突

对待哈希冲突,HashMap采用的链表 + 红黑树的形式。
ThreadlocalMap既没有链表,也没有红黑树,采用的是开放定址法 ,是这样,是如果发生冲突,ThreadlocalMap直接往后找相邻的下一个节点,如果相邻节点为空,直接存进去,如果不为空,继续往后找,直到找到空的,把元素放进去,或者元素个数超过数组长度阈值,进行扩容。

private void set(ThreadLocal<?> key, Object value) {
  Entry[] tab = table;
  int len = tab.length;
  // hashcode & 操作其实就是 %数组长度取余数,例如:数组长度是4,hashCode % (4-1) 就找到要存放元素的数组下标
  //ps:hash%length等于hash&(length-1)的前提是length是2的n次幂。
  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可能为空,做替换清理动作
    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);
}

强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它,当内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描内存区域时,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
虚引用虚引用顾名思义,就是形同虚设。与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
ThreadlocalMap 中key 设计成 WeakReference,为了尽最大努力避免内存泄漏。

Object angela = new Object();
//弱引用
 WeakReference<Object> weakReference = new WeakReference<>(angela);
//angela和弱引用指向同一个对象
System.out.println(angela);//java.lang.Object@4550017c
System.out.println(weakReference.get());//java.lang.Object@4550017c
//将强引用angela置为null,这个对象就只剩下弱引用了,内存够用,弱引用也会被回收
angela = null;
System.out.println(weakReference.get());//java.lang.Object@4550017c
System.gc();//内存够用不会自动gc,手动唤醒gc
System.out.println(angela);//null
System.out.println(weakReference.get());//null

private static final ThreadLocal< UserInfo > userInfoThreadLocal = new ThreadLocal<>();
userInfoThreadLocal 引用了ThreadLocal对象,这是个强引用,ThreadLocal对象同时也被ThreadlocalMap的key引用,这是个WeakReference引用,GC要回收ThreadLocal对象的前提是它只被WeakReference引用,没有任何强引用。

那既然ThreadLocal对象有强引用,回收不掉,干嘛还要设计成WeakReference类型呢?
ThreadLocal的设计者考虑到线程往往生命周期很长,比如经常会用到线程池,线程一直存活着,根据JVM 根搜索算法,一直存在 Thread -> ThreadLocalMap -> Entry(元素)这样一条引用链路, 如果key不设计成WeakReference类型,是强引用的话,就一直不会被GC回收,key就一直不会是null,不为null Entry元素就不会被清理(ThreadLocalMap是根据key是否为null来判断是否清理Entry)。
所以ThreadLocal的设计者认为只要ThreadLocal 所在的作用域结束了工作被清理了,GC回收的时候就会把key引用对象回收,key置为null,ThreadLocal会尽力保证Entry清理掉,来最大可能避免内存泄漏。

那如果Threadlocal 对象一直有强引用,那怎么办?岂不是有内存泄漏风险。
我们可以手动调用remove函数。

     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();//referent制空
                 expungeStaleEntry(i);//清理空槽
                 return;
             }
         }
     }
     private int expungeStaleEntry(int staleSlot) {
         Entry[] tab = table;
         int len = tab.length;

         // expunge entry at staleSlot  把staleSlot的value置为空,然后数组元素置为空
         tab[staleSlot].value = null;
         tab[staleSlot] = null;
         size--;

         // Rehash until we encounter null
         Entry e;
         int i;
         for (i = nextIndex(staleSlot, len);
              (e = tab[i]) != null;
              i = nextIndex(i, len)) {
             ThreadLocal<?> k = e.get();
             if (k == null) {   //k 为null代表引用对象被GC回收掉了
                 e.value = null;
                 tab[i] = null;
                 size--;
             } else {
                  //因为元素个数减少了,就把后面的元素重新hash
                 int h = k.threadLocalHashCode & (len - 1);
                //hash地址不相等,就代表这个元素之前发生过hash冲突(本来应该放在这没放在这)。
                //现在因为有元素被移除了,很有可能原来冲突的位置空出来了,重试一次。
                 if (h != i) {
                     tab[i] = null;
                     //继续采用链地址法存放元素
                     while (tab[h] != null)
                         h = nextIndex(h, len);
                     tab[h] = e;
                 }
             }
         }
         return i;
     }
threadLocal三大坑

1、内存泄露
2、线程池中线程上下文丢失
ThreadLocal不能在父子线程中传递,因此最常见的做法是把父线程中的ThreadLocal值拷贝到子线程中(线程不一样了,线程内的变量肯定传递不过去)。
3、并行流中线程上下文丢失
并行流底层的实现也是一个ForkJoin线程池,既然是线程池,那ContextHolder.get()可能取出来的就是一个NULL。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值