java 深入理解ThreadLocal

文章目录

  • 数据结构
  • ThreadLocal核心方法
    • get函数
    • set函数
    • remove函数
  • ThreadLocal使用实例
  • 引用关系
    • 为什么使用弱引用?
  • Web服务器中线程池的状态问题
    • 遇到的问题

ThreadLocal是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于 各个线程依赖不通的变量值完成操作的场景

ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程的上下文。

可以总结为一句话:ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度
举个例子,我出门需要先坐公交再做地铁,这里的坐公交和坐地铁就好比是同一个线程内的两个函数,我就是一个线程,我要完成这两个函数都需要同一个东西:公交卡(北京公交和地铁都使用公交卡),那么我为了不向这两个函数都传递公交卡这个变量(相当于不是一直带着公交卡上路),我可以这么做:将公交卡事先交给一个机构,当我需要刷卡的时候再向这个机构要公交卡(当然每次拿的都是同一张公交卡)。这样就能达到只要是我(同一个线程)需要公交卡,何时何地都能向这个机构要的目的

有人要说了:你可以将公交卡设置为全局变量啊,这样不是也能何时何地都能取公交卡吗?但是如果有很多个人(很多个线程)呢?大家可不能都使用同一张公交卡吧(我们假设公交卡是实名认证的),这样不就乱套了嘛。现在明白了吧?这就是ThreadLocal设计的初衷:提供线程内部的局部变量,在本线程内随时随地可取,隔离其他线程。

数据结构

java 深入理解ThreadLocal_第1张图片

从上面的结构图,我们已经窥见ThreadLocal的核心机制:

  • 每个Thread线程内部都有一个Map
  • Map里面存储线程本地对象(key)和线程的变量副本(value)
  • 但是,Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。

所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

ThreadLocal核心方法

get函数

该函数用来获取与当前线程关联的ThreadLocal的值,函数签名如下:
public T get()
如果当前线程没有该ThreadLocal的值,则调用initialValue函数获取初始值返回。

set函数

set函数用来设置当前线程的该ThreadLocal的值,函数签名如下:
public void set(T value)
设置当前线程的ThreadLocal的值为value。

remove函数

remove函数用来将当前线程的ThreadLocal绑定的值删除,函数签名如下:
public void remove()
在某些情况下需要手动调用该函数,防止内存泄露。

ThreadLocal的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();
  }

其中getMap的源码:

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
setInitialValue函数的源码:

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

createMap函数的源码:

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

简单解析一下,get方法的流程是这样的:

  1. 首先获取当前线程
  2. 根据当前线程获取一个Map
  3. 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的value e,否则转到5
  4. 如果e不为null,则返回e.value,否则转到5
  5. Map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map

ThreadLocal使用实例

学习了最基本的操作之后,我们用一段代码来演示ThreadLocal的用法,该例子实现下面这个场景:

有5个线程,这5个线程都有一个值value,初始值为0,线程运行时用一个循环往value值相加数字。

代码实现:

package com.winwill.test;

public class TestThreadLocal {
    private static final ThreadLocal value = new ThreadLocal() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(new MyThread(i)).start();
        }
    }
    static class MyThread implements Runnable {
        private int index;
        public MyThread(int index) {
            this.index = index;
        }
        public void run() {
            System.out.println("线程" + index + "的初始value:" + value.get());
            for (int i = 0; i < 10; i++) {
                value.set(value.get() + i);
            }
            System.out.println("线程" + index + "的累加value:" + value.get());
        }
    }
}

执行结果为:

线程0的初始value:0
线程3的初始value:0
线程2的初始value:0
线程2的累加value:45
线程1的初始value:0
线程3的累加value:45
线程0的累加value:45
线程1的累加value:45
线程4的初始value:0
线程4的累加value:45

可以看到,各个线程的value值是相互独立的,本线程的累加操作不会影响到其他线程的值,真正达到了线程内部隔离的效果。

引用关系

先交代一个事实:ThreadLocalMap是使用ThreadLocal的弱引用作为Key的:

static class ThreadLocalMap {
    
        static class Entry extends WeakReference> {
            /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
            }
        }
        ...
}

下图是本文介绍到的一些对象之间的引用关系图,实线表示强引用,虚线表示弱引用:

然后网上就传言,ThreadLocal会引发内存泄露,他们的理由是这样的:
如上图,ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄露。

为什么使用弱引用?

从表面上看,发生内存泄漏,是因为Key使用了弱引用类型。但其实是因为整个Entry的key为null后,没有主动清除value导致。很多文章大多分析ThreadLocal使用了弱引用会导致内存泄漏,但为什么使用弱引用而不是强引用?
官方文档的说法:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
为了处理非常大和生命周期非常长的线程,哈希表使用弱引用作为 key。

下面我们分两种情况讨论:

key 使用强引用:引用的ThreadLocal的对象被回收了(即key为null),但是ThreadLocalMap还持有ThreadLocal的强引用(牵狗的绳子还在),如果没有手动删除ThreadLocal的对象(狗),ThreadLocal不会被回收,导致Entry内存泄漏。
强引用实例

 List list = new ArrayList();
        Person a = new Person();
        a.id = 1;
        a.age = 10;
       
      HashMap mapPerson = new HashMap();
        mapPerson.put("aa",a);
        System.out.println(mapPerson);
        System.out.println(mapPerson.get("aa"));
        System.out.println(mapPerson.get("aa").getId());
        
        // 强引用 ,即使删除了引用a ,但其实对象在堆里还在。并且 mapPerson 还在,因此mapPerson.get("aa") 还在
        a = null;
//        a.id =25;

        System.out.println(mapPerson);
        System.out.println(mapPerson.get("aa").getId());
        System.out.println(a);

结果

{aa=Person@3f99bd52, bb=Person@4f023edb}
Person@3f99bd52
1
{aa=Person@3f99bd52, bb=Person@4f023edb}
1
null

key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set(),get(),remove()的时候会被清除。
比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key的value就会导致内存泄漏,而不是因为弱引用。
其实,在JDK的ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施,下面是ThreadLocalMap的getEntry方法的源码:

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

private int expungeStaleEntry(int staleSlot) {
           Entry[] tab = table;
           int len = tab.length;
           // expunge entry at staleSlot
           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) {
                   e.value = null;
                   tab[i] = null;
                   size--;
               } else {
                   int h = k.threadLocalHashCode & (len - 1);
                   if (h != i) {
                       tab[i] = null;
                       // Unlike Knuth 6.4 Algorithm R, we must scan until
                       // null because multiple entries could have been stale.
                       while (tab[h] != null)
                           h = nextIndex(h, len);
                       tab[h] = e;
                   }
               }
           }
           return i;
       }

整理一下ThreadLocalMap的getEntry函数的流程:

首先从ThreadLocal的直接索引位置(通过ThreadLocal.threadLocalHashCode & (len-1)运算得到)获取Entry e,如果e不为null并且key相同则返回e;
如果e为null或者key不一致则向下一个位置查询,如果下一个位置的key和当前需要查询的key相等,则返回对应的Entry,否则,如果key值为null,则擦除该位置的Entry,否则继续向下一个位置查询
在这个过程中遇到的key为null的Entry都会被擦除,那么Entry内的value也就没有强引用链,自然会被回收。仔细研究代码可以发现,set操作也有类似的思想,将key为null的这些Entry都删除,防止内存泄露。
但是光这样还是不够的,上面的设计思路依赖一个前提条件:要调用ThreadLocalMap的getEntry函数或者set函数。这当然是不可能任何情况都成立的,所以很多情况下需要使用者手动调用ThreadLocal的remove函数,手动删除不再需要的ThreadLocal,防止内存泄露。所以JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。

Web服务器中线程池的状态问题

Web服务在创建线程的过程中,频繁的创建线程对系能的影响巨大,故很多服务器都采用了线程池的方式来解决线程不断创建锁导致的问题,所以在使用线程池的过程中,由于线程是不断的回收和利用故ThreadLocal在服务器中也是被反复利用的,在使用中如果不进行清查操作,很容易导致变量污染。尽管对于很多服务器来说,ThreadLocal是的确是相对于每个线程,每个线程会有自己的ThreadLocal。但考虑到服务器都会维护一套线程池。因此,不同用户访问,可能会接受到同样的线程。因此,在做基于TheadLocal时,需要谨慎,避免出现ThreadLocal变量的缓存,导致其他线程访问到本线程变量,如果运用不当,会导致系统效率低下,举个例子,假设我们得系统在访问的时候在ThreadLocal中加入变量不予以更新和删除,则这个保存的对象就变成一个增量的容器对象,如果访问量巨大,将导致jvm内存不足而频繁触发gc,gc在工作的时候会进行数据复制,频繁的触发gc对系统的性能会带来不利影响,同时还有可能导致内存溢出。

遇到的问题

public class ContextHolder {

    private static ThreadLocal userContext = new InheritableThreadLocal();
    public static UserContext getUserContext(){
        if(userContext.get() == null){
            userContext.set(new UserContext());
        }
        return userContext.get();
    }

    public static void setContext(UserContext context) {
        userContext.set(context);
    }

    public static void clear(){
        userContext.remove();
    }
}

如果每个线程(访问请求)在结束的时候没有调用clear 方法的时候,其他线程再访问,就会造成线程污染,即拿到了其他线程的变量。

参考:
https://www.jianshu.com/p/98b68c97df9b (ThreadLocal-面试必问深度解析)
https://www.jianshu.com/p/a1cd61fa22da (ThreadLocal内存泄漏真因探究)
http://qifuguang.me/2015/09/02/[Java并发包学习七]解密ThreadLocal/
http://blog.csdn.net/chichengit/article/details/7994712
http://blog.csdn.net/lufeng20/article/details/24314381
http://www.importnew.com/22039.html
http://www.importnew.com/22046.html

你可能感兴趣的:(java,面试)