多线程与高并发(10)——ThreadLocal解析

一、简介

对于一个变量来说,任何一个线程都能够修改。如果想实现每一个线程都有自己的专属本地变量,这个本地变量只能自己获取修改,该如何解决呢?ThreadLocal就是解决了这个问题。
ThreadLocal类让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻数据盒子,盒子中可以存储每个线程的私有数据。
可以通过 ThreadLocal value = new ThreadLocal(); 来使用。然后会自动在每一个线程上面创建一个T类型的value副本,副本之间彼此独立,互不影响。
也就是说,访问这个变量的每个线程都会有这个变量的本地副本,这些副本不相互影响。可以使用 get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。
举个栗子:办公室每个人都有自己的办公电脑,不会用同一台电脑办公。

二、使用实例

代码如下:

static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    public static void main(String[] args) {
        new Thread(() -> {
            threadLocal.set("我是A线程");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(threadLocal.get());
        }).start();

        new Thread(() -> {
            threadLocal.set("我是B线程");
            System.out.println(threadLocal.get());
        }).start();
    }

输出结果如下:

我是B线程
我是A线程

可见AB线程中的值是互不影响的。

三、源码解析

我们先看下Thread类里的代码:

	//与此线程有关的ThreadLocal值。由ThreadLocal类维护
    ThreadLocal.ThreadLocalMap threadLocals = null;
    //此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

我们可以把 ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap。默认情况下这两个变量都是 null,只有当前线程调用 ThreadLocal 类的 set或get方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap类对应的 get()、set()方法。

看set源码,如下:

 public void set(T value) {
 		//获取当前线程
        Thread t = Thread.currentThread();
        //获取线程中的属性 threadLocalMap ,如果threadLocalMap 不为空,
        //则直接更新要保存的变量值,否则创建threadLocalMap,并赋值
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value。
threadLocalMap是什么呢,看以下代码:

static class ThreadLocalMap {

        /**
         * ThreadLocalMap是ThreadLocal的内部静态类,
         * 而它的构成主要是用Entry来保存数据 ,
         * 而且还是继承的弱引用。
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
......
		ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对。
为什么使用弱引用呢?我们接着往下看。
看get方法,如下:

public T get() {
        Thread t = Thread.currentThread();
        //获取当前线程的ThreadLocalMap 
        ThreadLocalMap map = getMap(t);
        if (map != null) {
        	//根据当前ThreadLocalMap 获取value的值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //如果是数据为null,则初始化
        //初始化的结果:TheralLocalMap中存放key值为threadLocal,值为null
        return 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;
    }

还有一个remove方法,如下:

public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

直接将ThrealLocal 对应的threadLocalMap中的value值从当前Thread中的ThreadLocalMap中删除。
为什么要删除呢?这个涉及内存泄露。

四、内存泄露问题

我们先看下ThreadLocal与Thread,ThreadLocalMap之间的关系 :
在这里插入图片描述
从上图中,我们总结以下几点(线程在运行时):
1、每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对。如果一个线程多个ThreadLocal的话,也是在同一个ThreadLocalMap中存储。
2、Entry继承的弱引用,所谓弱引用,所谓弱引用,一旦GC,则一定回收。也就是说,在Entry中,key值也就是threadLocal是弱引用的。为什么用弱引用呢?
解析: 如果Entry是强引用的话,如果使用完ThreadLocal对象了,GC这时候开始回收了,它先回收了thread当中的ThreadLocal的引用(因为不用了),在继续删除ThreadLocal的实际对象时,发现被ThreadLocalMap中的key强引用着,这时候就不会回收(只要线程运行,就永远被引用),多了就造成内存泄露。
3、 由于ThreadLocalMap 的生命周期跟 Thread 一样长,对于重复利用的线程来说,如果没有手动删除(remove()方法)对应 key 就会导致entry(null,value)的对象越来越多,从而导致内存泄漏。这又是为什么呢?
解析: 因为threadLocal是弱引用的,如果threadLocal被回收了,线程还在运行,这时候key为null,value是有值的,且无法访问到,堆积多的话也会导致内存泄露。这时候就得调用remove方法了。

事实上,在 ThreadLocalMap 中的set/getEntry 方法中,会对 key 为 null(也即是 ThreadLocal
为 null )进行判断,如果为 null 的话,那么会把 value 置为 null 的.这就意味着使用threadLocal ,
CurrentThread 依然运行的前提下.就算忘记调用 remove 方法,弱引用比强引用可以多一层保障:弱引用的
ThreadLocal 会被回收.对应value在下一次 ThreadLocaI 调用 get()/set()/remove()
中的任一方法的时候会被清除,从而避免内存泄漏.

所以,最好是在用完threadLocal后,就调用remove()方法防止内存泄露。

五、应用场景

ThreadLocal 适用于如下两种场景

1、每个线程需要有自己单独的实例
2、实例需要在多个方法中共享,但不希望被多线程共享

1、方便同一个线程使用某一对象,避免不必要的参数传递
在Spring的@Transaction事务声明的注解中就使用ThreadLocal保存了当前的Connection对象,避免在本次调用的不同方法中使用不同的Connection对象。
2、全局存储用户信息
可以尝试使用ThreadLocal替代Session的使用,当用户要访问需要授权的接口的时候,可以现在拦截器中将用户的Token存入ThreadLocal中;之后在本次访问中任何需要用户信息的操作都可以直接从ThreadLocal中拿取数据。
3、解决线程安全问题
Spring MVC 的 RequestContextHolder 的实现使用了ThreadLocal。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值