一篇文章教你彻底理解ThreadLocal


ThreadLocal是什么?

ThreadLocal是用于多线程环境下保证数据安全的一种辅助类(存储资源数据),它最底层是基于Entry<key,value>类型的数组存储数据,中间会包一层ThreadLocalMap。其依赖顺序为:ThreadLocal->ThreadLocalMap->Entry<key,value>[]

从使用层面来看,其实就是个存储数据的类,用ThreadLocal存储的对象,对于每个线程来说都有一份独立的数据。比如说:你往ThreadLocal存入一个对象A后,每个访问ThreadLocal的线程都各自取出对象A,线程相互间修改数据后互不干扰,保证了数据安全。


ThreadLocal如何使用?

实际使用上ThreadLocal一般都是作为类的静态常量去声明使用

private static final ThreadLocal<T> contextHolder = new ThreadLocal<>();

ThreadLocal有3个可调用的方法:get()、set()、remove() 分别对应往ThreadLocal里存对象、取对象以及删除对象。
在这里插入图片描述

特别注意

如果在线程池中使用ThreadLocal,在操作完后一定要调用remove()方法清除当前ThreadLocal数据,否则会引起内存泄露。 具体引起泄露原因放在下面分析!

ThreadLocal数据存储

有些人对ThreadLocal有一些简单的误区,认为ThreadLocal底层是由Map构成。这个说法不准确。上面介绍过说ThreadLocal最底层是由Entry<key,value>数组构成,这里的Entry<key,value>跟HashMap里的Entry<key,value>没有关系。

在这里插入图片描述

ThreadLocal底层存储数据直接存储在Entry数组中,大概的流程是这样:先对key(当前ThreadLocal对象)进行hash,计算出需要存储的Entry数组下标,判断对应数组下标是否有数据,如果没有则直接存储。如果有,则以当前数组下标起点往后遍历寻找数据为空的下标,找到了就存储,存储格式为:(key=当前ThreadLocal对象 、 value=要存储的值)。如果直到数组末尾还未找到为空的数组下标位置。那么就会停止。

  • ThreadLocal hash冲突后往数组后面遍历寻找数据为空的下标存储
  • HashMap hash冲突后,原数组下标位置生成一条链表(or 红黑树)往链表下面存储或遍历

先对key(当前ThreadLocal对象)进行hash,计算出被存储的数组下标位置。然后这里会有两种情况:

  • 判断当前数组下标内数据的key == 用于进行hash的当前ThreadLocal对象。则直接取出当前数组下标数据的value返回
  • 判断当前数组下标内数据的key != 用于进行hash的当前ThreadLocal对象。则往后遍历并判断找出数组下标内数据的key == 用于进行hash的当前ThreadLocal对象然后返回数组下标数据的value

ThreadLocal原理解析

上面的介绍说过ThreadLocal是多线程环境下保证数据安全的一种辅助类,它的实现原理其实很简单。先说结论
ThreadLocal底层依赖Thread类下名为:threadLocals的一个变量。ThreadLocal的get、set以及remove方法都是围绕这个threadLocals 变量进行的处理。也就是说我们在操作ThreadLocal时,往ThreadLocal里存的对象实际上都会存到当前Thread线程对象下的threadLocals字段里,由于每个线程对象都有一份threadLocals变量,从而实现了线程之间的数据隔离,一定意义上保证了数据安全。

简单贴一段基于ThreadLocal的get()方法的代码验证上面说法的源码,其余的set()、remove()方法可自行阅读

  public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //从当前线程中取出数据ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //这里的getMap(t);逻辑贴在下面
        if (map != null) {
            //从ThreadLocalMap中获取期望数据
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //如果当前线程没有ThreadLocalMap(存过ThreadLocal),则初始化
        return setInitialValue();
    }

    //获取当前线程的threadLocals字段并返回
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

Thread.threadLocals

上面介绍说过ThreadLocal实现线程数据隔离就是依靠线程类的threadLocals属性。通过阅读Thread类的部分源码可知,threadLocals属性的修饰类型其实是ThreadLocal类的一个子类ThreadLocalMap

ThreadLocal.ThreadLocalMap threadLocals = null;

原理

这里说一个因果关系再次论证上面的结论: ThreadLocal底层是基于ThreadLocalMap类型去存储数据(最下层是:entry<key,value>),而实际的数据会存储到当前Thread线程的threadLocals属性中。 当前Thread线程的threadLocals属性就是ThreadLocalMap类型 所以 当前线程在对ThreadLocal进行存取删操作,实际都是在操作当前线程本身的对象属性。这就实现了线程隔离,保证了数据安全。

Thread.inheritableThreadLocals

眼尖的人可能会发现在Thread.threadLocals属性下面还有个inheritableThreadLocals的属性,它的类型也是ThreadLocal.ThreadLocalMap。那它是用来干嘛的呢?

回答这个问题之前先思考一个问题,在一般场景下ThreadLocal帮助我们隔离了线程数据,保证了数据安全,这点没问题。那如果在线程池中或者当前线程的子线程中去使用ThreadLocal,由于ThreadLocal是跟每个线程绑定且数据隔离的,那线程池或者子线程中怎么能获取到外层的ThreadLocal对象呢?例如下面这种场景:

package com.example.study.threadLocal;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestThreadLocal {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    static {
        contextHolder.set("yxj");
    }

    public void test(){

        System.out.println("正常使用"+contextHolder.get());
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        executorService.execute(()->{
            System.out.println("线程池中使用"+contextHolder.get());
        });

       Runnable runnable = ()->{
           System.out.println("子线程中使用"+contextHolder.get());
       };
       Thread thread = new Thread(runnable);
       thread.start();
    }

    public static void main(String[] args) {
        TestThreadLocal testThreadLocal = new TestThreadLocal();
        testThreadLocal.test();
    }

}


上面这种代码场景应该不少见,在线程池&子线程中获取外层的数据进行操作。直接输出一下结果:
在这里插入图片描述
为了解决上面这种情况,Java提供了一个名为InheritableThreadLocal的类,它继承了ThreadLocal。它重写了ThreadLocal的getMap方法

    //重写前(ThreadLocal)
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
 
    //重写后(InheritableThreadLocal)
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

由原来的获取当前线程的threadLocals属性变成了获取当前线程的inheritableThreadLocals属性。也就是说如果使用了InheritableThreadLocal类,那么当你操作ThreadLocal时,实际上是操作当前线程的inheritableThreadLocals属性。

到这里你可能有些模糊,当前线程的inheritableThreadLocals属性和threadLocals属性它们都是ThreadLocalMap类型。为什么inheritableThreadLocals能解决上面那种情况的问题呢?

别急,先看效果再说原理,先将代码中ThreadLocal的实现类由ThreadLocal改成InheritableThreadLocal,其余代码保持不变
在这里插入图片描述
然后执行看下效果:
在这里插入图片描述
结果发现使用了InheritableThreadLocal就能在线程池中获取外面线程里的存进ThreadLocal的数据。实现了线程数据传递效果。

原理

InheritableThreadLocal相比其父类ThreadLocal类。达到实现线程数据传递目的的区别在于引用的当前线程属性不同。

  • InheritableThreadLocal —> Thread.inheritableThreadLocals
  • ThreadLocal —> Thread.threadLocals

但是最底层的真相是Thread线程类对于上面这两个属性有不同的处理,在创建线程对象时,“主”线程会将自身的inheritableThreadLocals属性传递给即将被创建的线程对象。但是对于threadLocals属性却不会传递。因此使用InheritableThreadLocal类能实现线程数据传递,解决了上面的那种情况。下面看代码:

  private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;

        //这里的parent就是当前线程(主),被创建的线程叫子线程
        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
           
            if (security != null) {
                g = security.getThreadGroup();
            }
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }
        g.checkAccess();      
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }

        g.addUnstarted();

        this.group = g;
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        this.target = target;
        setPriority(priority);
        //主要逻辑点在这,进行了一次主、子线程之间的数据传递,inheritThreadLocals默认是true
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        this.stackSize = stackSize;
        tid = nextThreadID();
    }

主要的逻辑点就是这段代码:
在这里插入图片描述

现在就能回答上面一开始的问题了,Thread.inheritableThreadLocals属性是用来干嘛的?
答:用来线程之前数据传递的,传递ThreadLocal对象。

ThreadLocal内存泄漏

ThreadLocal会发生内存泄漏的场景一般都是在线程池中使用ThreadLocal不当(线程处理完任务后没有remove掉ThreadLocal数据)导致的。

内存泄漏原因

我们先整个把ThreadLocal和Thread这两个类之间的关系串一串,首先ThreadLocal底层的存储是由<当前ThreadLocal对象, value>形式存储的,那么说明当前ThreadLocal对象直接引用着当前线程内的存储在ThreadLocal里的对象

如果当前ThreadLocal所在的对象类在GC引用链中不可达了,也就是需要被GC释放掉了。那么当GC过后,当前ThreadLocal不存在了。按照一般对象来说,引用我自身的XX对象不存在了,那么我自身也在GC引用链中不可达,我也应该被GC掉。但是!以ThreadLocal被存储的value数据来说,引用它的不止有key为当前ThreadLocal的引用。还有当前线程对象的threadLocals或者inheritableThreadLocals属性,只要当前线程对象不死亡,那么ThreadLocal中被存储的value数据也不会回收。这样就导致了本来应该需要被回收的对象,却一直无法回收(如果是在线程池中使用,那么该数据将永远无法被回收,除非线程池关闭!)。
在这里插入图片描述

小结论:

由于ThreadLocal中存储的数据被ThreadLocal本身和当前Thread线程对象双重引用,原始情况下ThreadLocal中存放的数据要被正确回收的两种强条件:

  • 当前线程死亡销毁,对ThreadLocal数据引用链 -1
  • 当前ThreadLocal对象所在的内存区域被回收,对ThreadLocal数据引用链 -1

所以如果当前线程不死亡 当前ThreadLocal所在的对象不被回收 (两个强引用) ,那么ThreadLocal中的数据将永远无法被回收。

对内存泄漏的补救

上面我们了解了内存泄漏出现的原因。ThreadLocal对这种情况进行了补救(为了避免内存泄漏),那就是存储数据进ThreadLocal时,将ThreadLocal本身作为一个WeakReference弱引用,去引用当前ThreadLocal数据。弱引用我们都知道,随时会被GC掉。所以这里就解了一个条件:不需要ThreadLocal对象所在的内存区域被回收。只需要当前线程死亡。失去最后一个强引用。剩下的弱引用则随时会被GC掉 结构如下图所示
在这里插入图片描述

所以看ThreadLocal底层Entry<key,value>类的声明,会继承一个WeakReference弱引用:
在这里插入图片描述

用完就要删除(最终解决)

上面的ThreadLocal补救措施虽然解决了一条强引用问题,但是并没有解决核心的问题,因为线程池中的核心线程是不会死亡的,这样ThreadLocal内存的数据也不会被回收,也还是会有内存泄漏问题。

到这种地步,已经没有办法帮我们自动进行管理回收了。需要我们在使用完ThreadLocal后。调用remove()方法进行手动回收数据。

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();
                    //核心
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

remove()方法会在对key(当前ThreadLocal对象)hash计算后清除对应的下标数组数据,以及以当前对应下标数组为起点,往前开始遍历,遍历到key为空(key是弱引用,随便一次GC就为空了,但是由于当前线程对象还持有引用,所以数据并不受影响)的数组下标,就将value设置为null。从而实现数据的删除,解决内存泄漏问题。

**此外:**除调用remove()会去检查并清空其他key为空的数组下标数据之外,get()、set()方法在hash冲突,往后遍历的情况下也会去检查其他key为空的数组下标数据并删除。删除数据的核心方法均为:expungeStaleEntry(int staleSlot)

private int expungeStaleEntry(int staleSlot){……}

小结论:
删除数据一前一后。调用remove()、get()、set()三个方法均会帮助我们删除已经无用的ThreadLocal数据。区别是:

  • remove()除了删除自身key所在的下标数据,还会往前遍历其他key为空的数据并删除
  • get()、set()方法不会删除自身key所在的下标数据,但是会往后遍历其他key为空的数据并删除

总结

以上就是本文对于ThreadLoca所有的总结!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值