【java多线程】5、ThreadLocal

一、ThreadLocal

在这里插入图片描述

1、概要:

  • JDK1.2开始支持java.lang.Threadlocal
  • 说明:一些变量只有当前线程可以访问,每个线程都有自己的变量副本。线程消亡他也消亡,他是变量对象,不是线程
  • 他可以在一些情况下代替多线程和线程同步机制
  • 使用案例:创建static的threadlocal变量,这样调用其他方法时就可以不用传某个参数了,直接去threadlocal中获取即可,可以保证线程安全
  • 如何做到的?:Thread类中有一个属性ThreadLocal.ThreadLocalMap,该属性key为每个threadlocal变量,value为该变量的值(当前线程)
    • 老版本的解决方案:每个threadlocal变量有个map,key为线程id,value为线程的值
img

2、ThreadLocal接口方法

// 创建案例
public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();

ThreadLoca类只有4个方法,支持泛型

  • void set(T value)
    设置当前线程的threadlocal的值
  • public T get()
    该方法返回当前线程threadlocal的值
  • public void removed()
    将当前线程threadlocal的值删除。
    目的是为了减少内存的占用。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度
  • protected T initialValue()
    返回该线程threadlocal的初始值,该方法是一个 protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用geto或 set(Object))时才执行,并且仅执行1次。 ThreadLocal中的默认实现直接返回一个null

创建threadlocal的同时指定初始值:

private static ThreadLocal<Integer> numberContainer = new ThreadLocal<Integer>() {
    @Override
    protected Integer initialValue() {//为什么 initialValue() 方法是 protected 的呢?就是为了提醒程序员们,这个方法是要你们来实现的,请给这个线程局部变量一个初始值吧。
        return 0;
    }
};

3、原理概览

public class Thread implements Runnable {
    // thread内部成员,每个线程都有一个map,s代表有可以保存多个threadLocal
    ThreadLocal.ThreadLocalMap threadLocals = null;
    // ThreadLocalMap是ThreadLocal的一个内部类,其作用相当于一个HashMap,用于保存隶属于该线程的变量副本。
}

虽然Thread中的ThreadLocalMap属性为空,但是并不需要我们手动创建赋值,因为set()或get()的时候会自动创建map

img

4、使用案例

案例1:threadlocal用作数据库连接

根本原因是数据库连接必须一个线程使用一个,防止线程不安全

我们使用数据库的时候首先就是建立数据库连接,然后用完了之后关闭就好了,这样做有一个很严重的问题,如果有1个客户端频繁的使用数据库,那么就需要建立多次链接和关闭,我们的服务器可能会吃不消,怎么办呢?如果有一万个客户端,那么服务器压力更大。

这时候最好ThreadLocal,因为ThreadLocal在每个线程中对连接会创建一个副本,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能

案例2:SimpleDateFormat

首先介绍SimpleDateFormat是什么,就是输出指定格式的日期

主要方法:

  • //创建:
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    
    // 把Date型的字符串 转换成 特定格式的String类型
    String str = sdf.format(new Date());
    // 2015-03-27 21:13:23
    
    //方法可以把String型的字符串 转换成 特定格式的date类型 
    Date date = sdf.parse("2015-03-27 21:13:23");
    
    

线程不安全:

该方法线程不安全,线程不安全的原因:如果该对象被多个线程操作,那么多个线程都是操作的其中的calendar属性

// 源码
protected Calendar calendar;

Date parse() {
    calendar.clear();
    ...; // 执行一些操作, 设置 calendar 的日期什么的
    calendar.getTime(); // 获取calendar的时间
}

SimpleDateFormat线程不安全的解决方案:

  • 使用同步(不推荐)
  • 使用ThreadLocal<SimpleDateFormat>
  • 使用线程安全的类:Instant 代替 Date, LocalDateTime 代替 Calendar,DateTimeFormatter 代替 SimpleDateFormat
/**
https://blog.csdn.net/weixin_42456466/article/details/80757114
 * 日期工具类(使用了ThreadLocal获取SimpleDateFormat,其他方法可以直接拷贝common-lang)
 */
public class DateUtil {

    private static Map<String,ThreadLocal<SimpleDateFormat>> sdfMap = new HashMap<String, ThreadLocal<SimpleDateFormat>>();

    private static Logger logger = LoggerFactory.getLogger(DateUtil.class);

    public final static String MDHMSS = "MMddHHmmssSSS";
    public final static String YMDHMS = "yyyyMMddHHmmss";
    public final static String YMDHMS_ = "yyyy-MM-dd HH:mm:ss";
    public final static String YMD = "yyyyMMdd";
    public final static String YMD_ = "yyyy-MM-dd";
    public final static String HMS = "HHmmss";

    /**
     * 根据map中的key得到对应线程的sdf实例
     * @param pattern map中的key
     * @return 该实例
     */
    private static SimpleDateFormat getSdf(final String pattern){
        ThreadLocal<SimpleDateFormat> sdfThread = sdfMap.get(pattern);// 获取指定日期格式的 当前线程的 SimpleDateFormat
        if (sdfThread == null){
            //双重检验,防止sdfMap被多次put进去值,和双重锁单例原因是一样的
            synchronized (DateUtil.class){
                sdfThread = sdfMap.get(pattern);
                if (sdfThread == null){
                    logger.debug("put new sdf of pattern " + pattern + " to map");
                    sdfThread = new ThreadLocal<SimpleDateFormat>(){
                        @Override
                        protected SimpleDateFormat initialValue() {
                            logger.debug("thread: " + Thread.currentThread() + " init pattern: " + pattern);
                            return new SimpleDateFormat(pattern);
                        }
                    };
                    sdfMap.put(pattern,sdfThread);
                }
            }
        }
        return sdfThread.get();
    }
    /**
     * 按照指定pattern解析日期
     * @param date 要解析的date
     * @param pattern 指定格式
     * @return 解析后date实例
     */
    public static Date parseDate(String date,String pattern){
        if(date == null) {
            throw new IllegalArgumentException("The date must not be null");
        }
        try {
            return  getSdf(pattern).parse(date);//返回Date
        } catch (ParseException e) {
            e.printStackTrace();
            logger.error("解析的格式不支持:"+pattern);
        }
        return null;
    }
    /**
     * 按照指定pattern格式化日期
     * @param date 要格式化的date
     * @param pattern 指定格式
     * @return 解析后格式
     */
    public static String formatDate(Date date,String pattern){
        if (date == null){
            throw new IllegalArgumentException("The date must not be null");
        }else {
            return getSdf(pattern).format(date);
        }
    }
}

4、TreadLocal优势:

ThreadLocal与 Thread同步机制的比较

synchronizedThreadLocal
原理同步机制采用以时间换空间的方式,只提供了一份变量, 让不同的线程排队访问ThreadLocal采用以空间换时间的方式, 为每一个线程都提供了一份变量的副本, 从而实现同访问而相不干扰
侧重点多个线程之间访问资源的同步多线程中让每个线程之间的数据相互隔离
为什么不适用session代替threadlocal

我们既然可以使用httpsession,为什么还要多此一举考虑ThreadLocal来管理session?

  • 1.HttpSession如果存放了大量的数据,会影响系统性能:
  • 2.我们在controller层使用HttpSession比较方便,当我们想在service层或者dao层使用时,就比较麻烦了,需要从controller传值

但是要注意tomcat线程复用的问题,解决方案:

  • 1、保证每次都用新的值覆盖线程变量;
  • 2、保证在每个请求结束后清空线程变量。

5、thread、threadlocal、map关系

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Orzpt25o-1615561115811)(https://i-blog.csdnimg.cn/blog_migrate/c34c3b8ba6e65c0fb9916187d48407ec.jpeg)]

上面这张图详细的揭示了ThreadLocalThread以及ThreadLocalMap三者的关系。

1、Thread中有一个map,就是ThreadLocalMap(map里只有一个Entry?不是的,可能有很多Entry,但与当前计算的threadlocal相关的entry只有一个)

2、ThreadLocalMap的key是ThreadLocal,值是我们自己设定的。

3、ThreadLocal是一个弱引用,内存不够时,会被当成垃圾回收,变为null

4、重点来了,突然我们ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此时我们的ThreadLocalMap生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。

解决办法:使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。

  • 每个线程里有个threadLocals变量,我们把ThreadLocalThread赋给threadLocals

二、源码

1、源码-属性

下面是ThreadLocal的类图结构,

img

从图中可知:Thread类中有两个变量

  • Thread.ThreadLocalMap threadLocals=null
  • Thread.ThreadLocalMap inheritableThreadLocals=null
说明 :
  • 为什么为空:由上面我们知道初始时两个map为空,那么为什么如需程序员自己创建map呢?因为调用set()或get()时会先确保但当前线程的map已经被创建。也就是懒加载
  • 缺点:线程不终止,map和值就还存在,所以可以调用remove方法手动提前删除threadLocals本地变量
  • 两个map的区别:类型一样

2、源码-方法

推荐阅读:https://www.cnblogs.com/fsmly/p/11020641.html

2.1、set()

顺序threadLocal的set方法:

// threadLocal.set
public void set(T value) {
    //(1)获取当前线程(调用者线程)
    Thread t = Thread.currentThread();
    //(2)以当前线程作为key值,去查找对应的线程变量,找到对应的map
    ThreadLocalMap map = getMap(t);//getMap直接返回t.threadLocals。即线程的属性threadLocals,该属性的类型是ThreadLocal.ThreadLocalMap // 每个线程的该属性初始值为null
    //(3)如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
    if (map != null)
        map.set(this, value);
    //(4)如果map为null,说明首次添加,需要首先创建出对应的map
    else // 创建同时添加
        createMap(t, value);
}

而map的set逻辑和hashMap的put差不多,都是按i进行哈希计算

不同点是:

  • i发生碰撞的话就后移一个元素位置放下
  • 如果该位置的key为空或者跟要放入的一致才放入成功
// ThreadLocalMap 
private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    // 获取索引值
    int i = key.threadLocalHashCode & (len-1);

    //遍历tab如果已经存在则更新值
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {//return ((i + 1 < len) ? i + 1 : 0); //ThreadLocal解决哈希冲突居然用的是i+1存储

        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        // 走到这说明k!=key
        if (k == null) { // 如果key为null的话,擦除
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    //如果上面没有遍历成功则创建新值
    tab[i] = new Entry(key, value);//以{threadlocal,value}作为键值对形参Entry
    int sz = ++size;
    //满足条件数组扩容x2
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}
2.2、getMap()、createMap()

getMap获取map(不是获取值)

ThreadLocalMap getMap(Thread t) {
    // 每个线程维护的threadLocals
    return t.threadLocals; 
}


// 第一个存threadlocal的时候创建ThreadLocalMap,这个map与当前线程关联,不需要传入key,因为第一个key是threadlocal
void createMap(Thread t, T firstValue) {
    //实例化一个新的ThreadLocalMap,并赋值给当前线程的成员变量threadLocals // 由这个s我们也能体会出每个线程内有多个threadLocal
    // 传入的this和firstValue会作为map中的第一个键值对
    t.threadLocals = new ThreadLocalMap(this, firstValue);// 创建后放到了Thread的属性中
}

为什么set的时候不需加锁:因为只是操作的当前线程的属性

2.3、get()

思路:

  • 拿到当前线程,
  • 获取当前线程里的map,即线程类里的threadLocals属性。
  • 如果map为空,创建map,同时把默认值传进去作为构造函数(构造完map顺便就把默认值设置进去了)
// 返回当前线程下threadLocalMap里以threadlocal为key的value值。、
// this为threadlocal具体变量
public T get() {
    //(1)获取当前线程
    Thread t = Thread.currentThread();
    //(2)获取当前线程的threadLocals变量
    ThreadLocalMap map = getMap(t);
    //(3)如果threadLocals变量不为null,就可以在map中查找到本地变量的值
    if (map != null) {
        // map中有多个threadlocal键值对,从map中拿到此threadlocal对应的值
        ThreadLocalMap.Entry e = map.getEntry(this);//当前索引key可能不等,那就判断下一索引位,因为我们创建时是这样解决哈希冲突的
        if (e != null) {
            //获取实体e对应的value值,即threadlocal值
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //(4)执行到此处,threadLocals为null,调用该更改初始化当前线程的threadLocals变量
    // 执行到这里的两种情况
    // 情况1:map不存在
    // 情况2:map存在,但没有与当前ThreadLocal关联的entry
    return setInitialValue();
}

// 情况1:map不存在
// 情况2:map存在,但没有与当前ThreadLocal关联的entry
private T setInitialValue() {
    //protected T initialValue() {return null;}
    T value = initialValue();
    //获取当前线程
    Thread t = Thread.currentThread();
    //以当前线程作为key值,去查找对应的线程变量,找到对应的map
    ThreadLocalMap map = getMap(t);
    //如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
    if (map != null)
        map.set(this, value);// this是当前threadlocal
    //如果map为null,说明首次添加,需要首先创建出对应的map
    else
        createMap(t, value);
    return value;
}
2.4、remove()

remove方法判断该当前线程对应的threadLocals变量是否为null,不为null就直接删除当前线程中指定的threadLocals变量

// 判断当前线程的ThreadLocalMap为不为空,不为空就主动移除掉map里的当前threadlocal。需要主动调用remove,否则会有内存溢出,即线程只要不消亡,threadlocal就还在,他的value也在
public void remove() {
    //获取当前线程绑定的threadLocals
    ThreadLocalMap m = getMap(Thread.currentThread());
    //如果map不为null,就移除当前线程中指定ThreadLocal实例的本地变量
    if (m != null)
        m.remove(this);
}

/** ThreadLocalMap类的方法,根据key删除对应的entry  */
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)]) {
        // key可能发生过碰撞,所以用nextIndex(i, len)计算下一个i
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

当前ThreadLocal的引用k被传递给WeakReference的构造函数,所以ThreadLocalMap中的key为ThreadLocal的弱引用。当一个线程调用ThreadLocal的set方法设置变量的时候,当前线程的ThreadLocalMap就会存放一个记录,这个记录的key值为ThreadLocal的弱引用,value就是通过set设置的值。如果当前线程一直存在且没有调用该ThreadLocal的remove方法,如果这个时候别的地方还有对ThreadLocal的引用,那么当前线程中的ThreadLocalMap中会存在对ThreadLocal变量的引用和value对象的引用,是不会释放的,就会造成内存泄漏。

3、ThreadLocalMap内部实现Entry

Thread.ThreadLocalMap内部实际上是一个Entry数组private Entry[] table,我们先看看Entry的这个内部类

Entry类特点:

  • 继承自WeakReference
  • 构造参数中value正常赋值给属性,而key是通过父类来存储的
  • key的存储方式是弱引用,弱引用的构造函数(引用对象,引用队列)。引用队列存放引用被回收的对象
  • 如果entry.get() == null则代表引用消失了,要把对应的entry 从table中移除
//public class ThreadLocal<T> {
static class  {
    static class Entry extends WeakReference<ThreadLocal<?>> { // 弱引用,弱引用在GC时一定被释放
        /** value就是和ThreadLocal绑定的 */
        Object value;

        //k:ThreadLocal的引用,被传递给WeakReference的构造方法
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    //WeakReference构造方法(public class WeakReference<T> extends Reference<T> )
    public WeakReference(T referent) {
        super(referent); //referent:ThreadLocal的引用
    }

    //Reference构造方法
    Reference(T referent) {
        this(referent, null);//referent:ThreadLocal的引用
    }

    Reference(T referent, ReferenceQueue<? super T> queue) {
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;//引用队列
    }

在上面的代码中,我们可以看出,当前ThreadLocal的引用k被传递给WeakReference的构造函数,所以ThreadLocalMap中的key为ThreadLocal的弱引用。当一个线程调用ThreadLocal的set方法设置变量的时候,当前线程的ThreadLocalMap就会存放一个记录,这个记录的key值为ThreadLocal的弱引用,value就是通过set设置的值。如果当前线程一直存在且没有调用该ThreadLocal的remove方法,如果这个时候别的地方还有对ThreadLocal的引用,那么当前线程中的ThreadLocalMap中会存在对ThreadLocal变量的引用和value对象的引用,是不会释放的,就会造成内存泄漏。

考虑这个ThreadLocal变量没有其他强依赖,如果当前线程还存在,由于线程的ThreadLocalMap里面的key是弱引用,所以当前线程的ThreadLocalMap里面的ThreadLocal变量的弱引用在gc的时候就被回收,但是对应的value还是存在的这就可能造成内存泄漏(因为这个时候ThreadLocalMap会存在key为null但是value不为null的entry项)。

总结:THreadLocalMap中的Entry的key使用的是ThreadLocal对象的弱引用,在没有其他地方对ThreadLoca依赖,ThreadLocalMap中的ThreadLocal对象就会被回收掉,但是对应的不会被回收,这个时候Map中就可能存在key为null但是value不为null的项,这需要实际的时候使用完毕及时调用remove方法避免内存泄漏。

//在某一线程声明了ABC三种类型的ThreadLocal
ThreadLocal<A> sThreadLocalA = new ThreadLocal<A>();
ThreadLocal<B> sThreadLocalB = new ThreadLocal<B>();
ThreadLocal<C> sThreadLocalC = new ThreadLocal<C>();
由前面我们知道对于一个Thread来说只有持有一个ThreadLocalMap,所以ABC对应同一个ThreadLocalMap对象。为了管理ABC,于是将他们存储在一个数组的不同位置,而这个数组就是上面提到的Entry型的数组table。

    那么问题来了,ABC在table中的位置是如何确定的?为了能正常够正常的访问对应的值,肯定存在一种方法计算出确定的索引值i

4、内存泄露问题

ThreadLocal.ThreadLocalMap,其中的key为当前定义的ThreadLocal变量的this引用,value为我们使用set方法设置的值。

每个线程的本地变量存放在自己的本地内存变量Map中,如果当前线程一直不消亡,那么这些本地变量就会一直存在(所以可能会导致内存溢出),因此使用完毕需要将其remove掉。

ThreadLocalMap是ThreadLocal的静态内部类, 没有实现Map接口, 用独立的方式实现了Map的功能, 其内部的Entry也是独立实现.

在这里插入图片描述

要搞懂问题需要我们了解的问题有:JVM四种引用、ThreadLocalMap内部构造

  • 内存溢出overflow
  • 内存泄露leak,无法回收

JVM四种引用:

  • ①强引用:Java中默认的引用类型,一个对象如果具有强引用那么只要这种引用还存在就不会被GC。
  • ②软引用:简言之,如果一个对象具有弱引用,在JVM发生OOM之前(即内存充足够使用),是不会GC这个对象的;只有到JVM内存不足的时候才会GC掉这个对象。软引用和一个引用队列联合使用,如果软引用所引用的对象被回收之后,该引用就会加入到与之关联的引用队列中
  • ③弱引用(这里讨论ThreadLocalMap中的Entry类的重点):如果一个对象只具有弱引用,那么这个对象就会被垃圾回收器GC掉(被弱引用所引用的对象只能生存到下一次GC之前,当发生GC时候,无论当前内存是否足够,弱引用所引用的对象都会被回收掉)。弱引用也是和一个引用队列联合使用,如果弱引用的对象被垃圾回收期回收掉,JVM会将这个引用加入到与之关联的引用队列中。若引用的对象可以通过弱引用的get方法得到,当引用的对象呗回收掉之后,再调用get方法就会返回null④虚引用:虚引用是所有引用中最弱的一种引用,其存在就是为了将关联虚引用的对象在被GC掉之后收到一个通知。(不能通过get方法获得其指向的对象)

threadlocal里面使用了一个存在弱引用的map,当释放掉threadlocal的强引用以后,map里面的value却没有被回收.而这块value永远不会被访问到了. 所以存在着内存泄露.

正确的用法:将调用threadlocal的remove(): 把当前ThreadLocal从当前线程的ThreadLocalMap中移除。(包括key,value)

当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收.

所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露.

为什么key使用弱引用

https://blog.csdn.net/puppylpg/article/details/80433271

不妨反过来想想,如果使用强引用,当ThreadLocal对象(假设为ThreadLocal@123456)的引用(即:TL_INT,是一个强引用,指向ThreadLocal@123456)被回收了,ThreadLocalMap本身依然还持有ThreadLocal@123456的强引用,如果没有手动删除这个key,则ThreadLocal@123456不会被回收,所以只要当前线程不消亡,ThreadLocalMap引用的那些对象就不会被回收,可以认为这导致Entry内存泄漏。

那使用弱引用的好处呢?

如果使用弱引用,那指向ThreadLocal@123456对象的引用就两个:TL_INT强引用,和ThreadLocalMap中Entry的弱引用。一旦TL_INT被回收,则指向ThreadLocal@123456的就只有弱引用了,在下次gc的时候,这个ThreadLocal@123456就会被回收。

那么问题来了,ThreadLocal@123456对象只是作为ThreadLocalMap的一个key而存在的,现在它被回收了,但是它对应的value并没有被回收,内存泄露依然存在!而且key被删了之后,变成了null,value更是无法被访问到了!针对这一问题,ThreadLocalMap类的设计本身已经有了这一问题的解决方案,那就是在每次get()/set()/remove()ThreadLocalMap中的值的时候,会自动清理key为null的value。如此一来,value也能被回收了。

既然对key使用弱引用,能使key自动回收,那为什么不对value使用弱引用?答案显而易见,假设往ThreadLocalMap里存了一个value,gc过后value便消失了,那就无法使用ThreadLocalMap来达到存储全线程变量的效果了。(但是再次访问该key的时候,依然能取到value,此时取得的value是该value的初始值。即在删除之后,如果再次访问,取到null,会重新调用初始化方法。)

强引用能解决内存泄露吗

假设ThreadLocalMap中的key使用了强引用,那么会出现内存泄露吗?

  • threadlocal是作为key的,是我们自己定义的,也就是说有时候threadlocal变量是不消失的,如静态变量
  • threadlocal不消失,那么就永远可以查到。但是不用的时候就threadlocal本身就不会GC了,因为key指向threadlocal
  • 如果是弱引用,threadlocal=null后,就是说这个threadlocal永远不用了,但是entry还在,也就是说,entry是线程的里map的问题,不是threadlocal的问题。
  • 也就是说,ThreadLocalMap中的key使用了强引用,是无法完成避免内存泄露的

  • 线程运行时,我们定义的TheadLocal对象被初始化,存储在Heap,同时线程运行的栈区保存了指向该实例的引用,也就是图中的ThreadLocalRef
  • 当ThreadLocal的set/get被调用时,虚拟机会根据当前线程的引用也就是CurrentThreadRef找到其对应在堆区的实例,然后查看其对用的TheadLocalMap实例是否被创建,如果没有,则创建并初始化。
  • Map实例化之后,也就拿到了该ThreadLocalMap的句柄,然后如果将当前ThreadLocal对象作为key,进行存取操作
  • 图中的虚线,表示key对ThreadLocal实例的引用是个弱引用

如上图所示,我们在作为key的ThreadLocal对象没有外部强引用,下一次gc必将产生key值为null的数据,若线程没有及时结束必然出现,一条强引用链
Threadref–>Thread–>ThreadLocalMap–>Entry(下面那条),所以这将导致内存泄漏。

5、ThreadLocal子线程继承性问题

同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。(threadLocals中为当前调用线程对应的本地变量,所以二者自然是不能共享的)

public class ThreadLocalTest2 {

    //(1)创建ThreadLocal变量
    public static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        //在main线程中添加main线程的本地变量
        threadLocal.set("mainVal");
        //新创建一个子线程
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子线程中的本地变量值:"+threadLocal.get());
            }
        });
        thread.start();
        //输出main线程中的本地变量值
        System.out.println("mainx线程中的本地变量值:"+threadLocal.get());
    }
}

在上面说到的ThreadLocal类是不能提供子线程访问父线程的本地变量的,而InheritableThreadLocal类则可以做到这个功能,下面是该类的源码

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    protected T childValue(T parentValue) {
        return parentValue;
    }

    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

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

从上面代码可以看出,InheritableThreadLocal类继承了ThreadLocal类,并重写了childValue、getMap、createMap三个方法。其中createMap方法在被调用(当前线程调用set方法时得到的map为null的时候需要调用该方法)的时候,创建的是inheritableThreadLocal而不是threadLocals。同理,getMap方法在当前调用者线程调用get方法的时候返回的也不是threadLocals而是inheritableThreadLocal。

6、源码其他内容

  • key可能发生过碰撞,所以用nextIndex(i, len)计算下一个i

静态内部类和非静态内部类之间区别:

  1. 内部静态类不需要有指向外部类的引用。但非静态内部类需要。
  2. 静态类只能访问外部类的静态成员,非静态内部类能够访问外部类的静态和非静态成员。
  3. 非静态内部类不能脱离外部类实体被创建,非静态内部类可以访问外部类的数据和方法,因为他就在外部类里面。
public class ThreadLocal<T> {
    /**
    threadLocalHashCode是当前threadlocal的哈希值,每次调用它的适合都会更新一个新值。
    更新方法为从0开始,每获取一次就+0x61c88647。为什么是这个增量值?因为他哈希值平均
     */
    private final int threadLocalHashCode = nextHashCode();

    // 下一个哈希值,自动更新,从0开始
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /**
    因为static的原因,在每次new ThreadLocal时因为threadLocalHashCode的初始化,会使threadLocalHashCode值自增一次,增量为0x61c88647。

0x61c88647是斐波那契散列乘数,它的优点是通过它散列(hash)出来的结果分布会比较均匀,可以很大程度上避免hash冲突,已初始容量16为例,hash并与15位运算计算数组下标结果如下:(16进制取最后一位)
            hashCode	数组下标
            0x61c88647	7
            0xc3910c8e	14
            0x255992d5	5
            0x8722191c	12
            0xe8ea9f63	3
            0x4ab325aa	10
            0xac7babf1	1
            0xe443238	8
            0x700cb87f	15

     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     常量,在实例化完成之前有值即可
     扩展:一个thread内的hashCode是按上面的顺序创建的吗?答案为不是,因为ThreadLocal多线程可以交叉调用
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    // 获取下一个哈希值
    private static int nextHashCode() {
        // nextHashCode是AtomicInteger类型的,保证了
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

    // 当前线程的threadlocalMap中key为threadlocal的默认值,这里默认值为null // 如果想改变默认值,我们可以继承类后重写该方法
    protected T initialValue() {
        return null;
    }

    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }

    // 空构造
    public ThreadLocal() {}
    

    /**
     * Factory method to create map of inherited thread locals.
     * Designed to be called only from Thread constructor.
     * 
     * @param  parentMap 父线程关联的map
     * @return a map containing the parent's inheritable bindings
     */
    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }

    /**
     * Method childValue is visibly defined in subclass
     * InheritableThreadLocal, but is internally defined here for the
     * sake of providing createInheritedMap factory method without
     * needing to subclass the map class in InheritableThreadLocal.
     * This technique is preferable to the alternative of embedding
     * instanceof tests in methods.
     */
    T childValue(T parentValue) {
        throw new UnsupportedOperationException();
    }
//------------分割线------------------
    /**
     * An extension of ThreadLocal that obtains its initial value from
     * the specified {@code Supplier}.
     */
    static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {

        private final Supplier<? extends T> supplier;

        SuppliedThreadLocal(Supplier<? extends T> supplier) {
            this.supplier = Objects.requireNonNull(supplier);
        }

        @Override
        protected T initialValue() {
            return supplier.get();
        }
    }
//------------分割线------------------
    /**
    
    ThreadLocal的静态内部类ThreadLocalMap为每个Thread都维护了一个数组table,ThreadLocal确定了一个数组下标,而这个下标就是value存储的对应位置。。
    ThreadLocalMap是ThreadLocal的静态内部类, 没有实现Map接口, 用独立的方式实现了Map的功能, 其内部的Entry也是独立实现.
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.
     */
    //只有内部类可以为static
    static class ThreadLocalMap {



        // 懒加载的初始容量,编译期就确定的常量
        private static final int INITIAL_CAPACITY = 16;
        // 这个数组的容量也必须是2的幂
        private Entry[] table;
        // table内entries的数量
        private int size = 0;
        // 扩容阈值,跟hashmap的阈值同理
        private int threshold; // Default to 0
        //设置阈值为2/3容量,len为容量
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

        // 获取下一个坐标
        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);
        }

        // 当当前线程第一个调用threadlocal的方法时,创建map,且把传入的值作为第一个threadlocal键值对 // map是懒加载的
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            // 
            //初始容量为16
            table = new Entry[INITIAL_CAPACITY];
            //位运算,计算出需要存放的位置table[i] // 第一个并不是存在table[0]
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

        /**
         * Construct a new map including all Inheritable ThreadLocals
         * from given parent map. Called only by createInheritedMap.
         *
         * @param parentMap the map associated with parent thread.
         */
        private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

        /**
         * Get the entry associated with key.  This method
         * itself handles only the fast path: a direct hit of existing
         * key. It otherwise relays to getEntryAfterMiss.  This is
         * designed to maximize performance for direct hits, in part
         * by making this method readily inlinable.
         *
         * @param  key the thread local object
         * @return the entry associated with key, or null if no such
         */
        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);
        }
        // 走到这是因为get的时候索引位置没有entry,或者虽然有entry但是threadlocal不对
        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);//i+1继续判断
                e = tab[i];
            }
            return null;
        }


      
        /**
         * Replace a stale entry encountered during a set operation
         * with an entry for the specified key.  The value passed in
         * the value parameter is stored in the entry, whether or not
         * an entry already exists for the specified key.
         *
         * As a side effect, this method expunges all stale entries in the
         * "run" containing the stale entry.  (A run is a sequence of entries
         * between two null slots.)
         *
         * @param  key the key
         * @param  value the value to be associated with key
         * @param  staleSlot index of the first stale entry encountered while
         *         searching for key.
         */
        private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            // Back up to check for prior stale entry in current run.
            // We clean out whole runs at a time to avoid continual
            // incremental rehashing due to garbage collector freeing
            // up refs in bunches (i.e., whenever the collector runs).
            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;

            // Find either the key or trailing null slot of run, whichever
            // occurs first
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                // If we find key, then we need to swap it
                // with the stale entry to maintain hash table order.
                // The newly stale slot, or any other stale slot
                // encountered above it, can then be sent to expungeStaleEntry
                // to remove or rehash all of the other entries in run.
                if (k == key) {
                    e.value = value;

                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // Start expunge at preceding stale entry if it exists
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                // If we didn't find stale entry on backward scan, the
                // first stale entry seen while scanning for key is the
                // first still present in the run.
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // If key not found, put new entry in stale slot
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // If there are any other stale entries in run, expunge them
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

        /**
         * Expunge a stale entry by rehashing any possibly colliding entries
         * lying between staleSlot and the next null slot.  This also expunges
         * any other stale entries encountered before the trailing null.  See
         * Knuth, Section 6.4
         *
         * @param staleSlot index of slot known to have null key
         * @return the index of the next null slot after staleSlot
         * (all between staleSlot and this slot will have been checked
         * for expunging).
         */
        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;
        }

        /**
         * Heuristically scan some cells looking for stale entries.
         * This is invoked when either a new element is added, or
         * another stale one has been expunged. It performs a
         * logarithmic number of scans, as a balance between no
         * scanning (fast but retains garbage) and a number of scans
         * proportional to number of elements, that would find all
         * garbage but would cause some insertions to take O(n) time.
         *
         * @param i a position known NOT to hold a stale entry. The
         * scan starts at the element after i.
         *
         * @param n scan control: {@code log2(n)} cells are scanned,
         * unless a stale entry is found, in which case
         * {@code log2(table.length)-1} additional cells are scanned.
         * When called from insertions, this parameter is the number
         * of elements, but when from replaceStaleEntry, it is the
         * table length. (Note: all this could be changed to be either
         * more or less aggressive by weighting n instead of just
         * using straight log n. But this version is simple, fast, and
         * seems to work well.)
         *
         * @return true if any stale entries have been removed.
         */
        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;
        }

        /**
         * Re-pack and/or re-size the table. First scan the entire
         * table removing stale entries. If this doesn't sufficiently
         * shrink the size of the table, double the table size.
         */
        private void rehash() {
            expungeStaleEntries();

            // Use lower threshold for doubling to avoid hysteresis
            if (size >= threshold - threshold / 4)
                resize();
        }

        // 扩容table容量×2
        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 {
                        // 获取新的索引位置,原来的位置或者+oldLen的位置
                        int h = k.threadLocalHashCode & (newLen - 1);
                        while (newTab[h] != null)
                            // 如果当前位置有值了就存在下一索引位置
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }

            setThreshold(newLen);
            size = count;
            table = newTab;
        }

        /**
         * Expunge all stale entries in the table.
         */
        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);
            }
        }
    }
}
子线程访问不了主线程的threadlocal

同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。(threadLocals中为当前调用线程对应的本地变量,所以二者自然是不能共享的)

public class ThreadLocalTest2 {

    //(1)创建ThreadLocal变量
    public static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        //在main线程中添加main线程的本地变量
        threadLocal.set("mainVal");
        //新创建一个子线程
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子线程中的本地变量值:"+threadLocal.get());
            }
        });
        thread.start();
        //输出main线程中的本地变量值
        System.out.println("mainx线程中的本地变量值:"+threadLocal.get());
    }
}

InheritableThreadLocal

在上面说到的ThreadLocal类是不能提供子线程访问父线程的本地变量的,而InheritableThreadLocal类则可以做到这个功能,下面是该类的源码

// 注意操作的是inheritableThreadLocals属性即可
public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    protected T childValue(T parentValue) {
        return parentValue;
    }

    ThreadLocalMap getMap(Thread t) {
        return t.inheritableThreadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}
思路整理:
  • 每个线程有每个线程的ThreadLocalMap,

  • map 的key是threadLocal值,

  • map的value是Entry类型,而Entry.value是真正的值

  • set方法:先根据当前线程找到当前线程的ThreadLocalMap,

    • 如果有map:把threadLocal值作为key更新值。如果地址冲突后索引值+1

    • 如果没有map:new ThreadLocalMap(ThreadLocal变量, value);

    • Java为了最小化减少内存泄露的可能性和影响,在ThreadLocal的get,set的时候都会清除线程Map里所有key为null的value(get 方法会在遍历的时候如果遇到key为null,就调用expungeStaleEntry方法擦除,set方法在遍历的时候,如果遇到key为null,就调用replaceStaleEntry方法替换掉。见下面代码)。

      所以最怕的情况就是,threadLocal对象设null了,开始发生“内存泄露”,然后使用线程池,这个线程结束,线程放回线程池中不销毁,这个线程一直不被使用,或者分配使用了又不再调用get,set方法,那么这个期间就会发生真正的内存泄露。

  • get方法:先获取map,然后从map中获取ThreadLocal变量对应的值Entry

    • 获取到后然后entry.value
    • 获取不到设置entry为null//设置为null,我们可以创建子类重写初始值
  • remove方法:获取map后移除threadLocal值

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值