一个场景Demo分析ThreadLocal使用方法和原理

ThreadLocal使用比较常见,但是一个觉得这个东西哪里怪怪的,给人的感觉不是特别直观,本文通过一个常见使用场景,来分析其来龙去脉。

1.定义&作用

定义: ThreadLocal叫做线程本地变量,顾名思义,就是Thread的一个内部变量,这个ThreadLocal是属于某个线程的。就好比是某个人的老婆(们),老婆只能属于某个人,而不能被共享;这里老婆有可能是复数,一个人可以有多个老婆(好比在古代),老婆跟着丈夫走,丈夫生而生,丈夫死而死;
作用: 既然是属于某一个线程的变量,那么肯定就不是被多个线程共享的;这个东西在多线程场景中使用,目的就是防止变量在多个线程中游走,产生线程安全的问题。

下面描述一个场景:
在web项目中,比如常用的springmvc项目,每次请求服务端,tomcat都会使用一个单独的线程来处理这个请求。假如两个用户,用户A和用户B,都打开了一款app进入首页,假设首页数据是一个接口返回的,那么这两个用户就分别发起了一次请求,对服务端来说,就是两次请求。

这两个请求,同时来到后台服务端,是两个单独的线程threadA和threadB来处理的。假设首页的数据需要使用用户信息,比如userId(用户id),accountId(账户id),有很多业务方法都需要这个userId,accountId。比如进入首页的controller方法后,又依次调用了A.a()–>B.b()–>C.c()方法,最终将数据返回,但是a(),b(),c()这3个方法,都需要userId,accountId,这个时候userId,accountId该怎样获取呢?

这里简单一点,假设前端传进来了userId,我们还不知道accountId,这个时候有两种处理方式:

  • 1.在a(),b(),c()中用到accountId的时候,都使用userId去数据库(或者缓存啥的)中查询一次;这样可以完成任务,但是有两次查询时多余的,增加了接口耗时,这时如果业务增加了d/e/f方法也要用到accounId,那么就要增加更多次不必要的查询。
  • 2.使用ThreadLocal。我们可以在a()方法(当然更好的是在a之前,比如增加拦截器,在拦截器中查询)查询到accountId后,就放到当前线程的threadLocal中存储,这样当此线程的业务往下执行到bcdf方法时,如果需要accountId,直接从线程的threadLoca变量中查询即可。

2.使用Demo

说到这里,ThreadLocal的使用场景,就基本明确了,针对这种场景,下面上个Demo直接感受一下:

public class ThreadLocalDemo {

	public static void main(String[] args) {
        //起两个线程,模拟两个用户发起请求到web服务,
        // 比如两个用户都在请求首页接口
        //这时web服务端会有两个线程对应处理两个请求
        for (int i = 0; i < 2; i++) {
            int fn = i;
            new Thread(() -> {
                ServiceA.a("accountId" + fn);
                ServiceB.b();
                ServiceC.c();
            }, "线程_" + i).start();
        }
    }
	/**
     * 一般使用一个全局静态类管理ThreadLocal对象
     */
    static class ResourceClass {

        public final static ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
        public final static ThreadLocal<String> threadLocal2 = new ThreadLocal<>();

        public static void setThreadLocal1(String value) {
            ResourceClass.threadLocal1.set(value);
        }
        public static String getThreadLocal1() {
            return ResourceClass.threadLocal1.get();
        }

        public static void setThreadLocal2(String value) {
            ResourceClass.threadLocal2.set(value);
        }
        public static String getThreadLocal2() {
            return ResourceClass.threadLocal2.get();
        }
    }

	/**
     * 业务类A
     */
    static class ServiceA {
        /**
         * 模拟业务,获取到accountId,并保存到threadLocal1变量中
         * 同时保存线程名字到threadLocal2中
         */
        public static void a(String accountId) {
            String name = Thread.currentThread().getName();
            ResourceClass.setThreadLocal1(accountId);
            ResourceClass.setThreadLocal2(name);
            System.out.println(name + "在A类中保存accountId: " + accountId + " 到ThreadLocal\n");
        }
    }
	/**
     * 业务类B
     */
    static class ServiceB {
        /**
         * 模拟执行一些业务,要用到accountId
         */
        public static void b() {
            String accountId = ResourceClass.getThreadLocal1();
            String name = ResourceClass.getThreadLocal2();
            System.out.println(name + "在B类利用accountId:" + accountId + " 继续执行一些B类业务\n");
        }
    }
	/**
     * 业务类C
     */
    static class ServiceC {
        /**
         * 模拟执行一些业务,要用到accountId
         */
        public static void c() {
            String accountId = ResourceClass.getThreadLocal1();
            String name = ResourceClass.getThreadLocal2();
            System.out.println(name + "在C类利用accountId:" + accountId + " 继续执行一些C类业务\n");
        }
    }
}

结果:

线程_1在A类中保存accountId: accountId1 到ThreadLocal

线程_0在A类中保存accountId: accountId0 到ThreadLocal

线程_0在B类利用accountId:accountId0 继续执行一些B类业务

线程_1在B类利用accountId:accountId1 继续执行一些B类业务

线程_0在C类利用accountId:accountId0 继续执行一些C类业务

线程_1在C类利用accountId:accountId1 继续执行一些C类业务

上述Demo中,

  • 我们在main方法中开了两个线程来模拟处理前端的两个请求;线程_0保存的是accountId0,得到的也是accountId0;线程_1保存的是accountId1,得到的也是accountId1;最终也可以说明,threadLocal在多个线程中是独立不相互影响的;
  • ResourceClass中,我们new了两个threadLocal对象,一般我们使用一个threadLocal对象即可(里面存map),这里主要像说明Thread类中的threadLocals变量是支持存储多个threadLocal对象的;
  • 使用的多个业务类,是为了说明threadLocal的生命周期是跟着线程走的,在线程中的任何地方都可以获取到线程本地变量中的内容。

3.源码分析

下面直接看源码,
由于ThreadLocal叫做线程本地变量,那么说明就是Thread.java的一个内部属性,打开Thread看下:
在这里插入图片描述
在Thread类中,我们可以找到如上信息,变量名:threadLocals,复数说明一个线程可以有多个threadLocal,也就是说ThreadLocal.ThreadLocalMap可以保存多个ThreadLocal,那么我们打开看看是啥:
在这里插入图片描述
是ThreadLocal一个内部类,仔细看过其内容后,发现ThreadLocalMap就是个map,跟Hashmap类似,这里是另外一种map。
这里ThreadLocalMap是内部类,其实我们可以认为它就是一个map,和ThreadLocal类没有任何关系。因为ThreadLocal类只不过是一个工具类,提供了若干方法,来操作Thread类中的threadLocalMap对象。

ThreadLocalMap中有个内部类Entry ,因为是map,Entry类就是用来key和value值的,源码如下:

//继承了弱引用WeakReference
static class Entry extends WeakReference<ThreadLocal<?>> {
   /** The value associated with this ThreadLocal. */
    Object value;
    //构造函数
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

key是ThreadLocal对象本身,在本文的Demo中,就是threadLocal1和threadLocal2指向的对象(new 的 ThreadLocal 对象),value就是accountId。

Entry继承了弱引用WeakReference(弱引用指向的对象在GC时会被回收,如果不清楚弱引用,可参考文章《弱引用WeakReference作用与使用场景》),使用弱引用的目的,咱们结合本文demo来说。同时参考如下一张关系图:
在这里插入图片描述
我们把本文demo和上图联系起来:

  • Heap中的ThreakLocal对象:就是本demo中new的两个threadLocal对象;
  • ThreadLocalRef :就是demo中的threadLocal1和threadLocal2,它与heap中的ThreadLocal对象之间是实线,表示强引用;
  • key: 因为Entry是继承了弱引用,key保存的也是new的threadLocal对象,但他们关系是弱引用,虚线表示;
  • value:本demo中就是accountId;
  • CurrentThreadRef:本文demo中启了两个线程。线程_0和线程_1,CurrentThreadRef就是指向线程对象的,属于操作系统层面;
  • CurrentThread:对应demo中的线程_0和线程_1;他可以被ThreadLocal对象使用Thread.currentThread()方法获取到,进而操作线程中的map对象;
  • Map:线程_0和线程_1中都有个map对象,就是ThreadLocalMap类型的那个;

然后我们再说为啥ThreadLocalMap中的Entry为啥是弱引用,
因为当上图中ThreadLocalRef,也就是threadLocal1和threadLocal2假如被赋值了null(threadLocal1=null),这个时候heap中的ThreakLocal对象理论上应该成为垃圾被GC回收,但是,ThreakLocal对象此时还被key引用着呢,如果之间是强引用关系,ThreakLocal对象就不会被回收,但实际上业务又用不着这个对象了,这就产生了不能被回收的对象,造成内存泄漏;为了避免这个,只能使用弱引用。

当然使用弱引,虽然避免了ThreakLocal不能被回收,但是上图中的value值还在内存中,只要线程不销毁(比如线程池中的核心线程),那么map就一直存在,map中的Entry对象就一直存在,那么Entry中value对象就一直存在,依然会有内存泄漏的问题,因此,我们使用完map中的Entry对象时,要使用ThreadLocal提供的remove()方法将其从内存中移除,下面就让我们继续分析ThreadLocal类提供的几个常用方法。

下面看下ThreadLocal工具类中的几个常用方法:

3.1 set(T value)

public void set(T value) {
	//得到当前线程对象
    Thread t = Thread.currentThread();
    //从当前线程中get到内部变量ThreadLocalMap 类型的map
    ThreadLocalMap map = getMap(t);
    if (map != null)
    //如果map不是空,那么就直接把value Set进去,key是当前ThreadLocal对象
        map.set(this, value);
    else
    //如果map是空,就初始化,创建一个map
        createMap(t, value);
}

上面方法逻辑比较简单,直接看注释。其中有几个方法,需要打开看下:

  • getMap(t):
ThreadLocalMap getMap(Thread t) {
     return t.threadLocals;
  }

就是返回当前Thread线程对象中的threadLocals变量。

  • map.set(this, value)
    ThreadLocalMap的set方法,
private void set(ThreadLocal<?> key, Object value) {

     Entry[] tab = table;
     int len = tab.length;
     //计算value在map数组中的插入位置
     int i = key.threadLocalHashCode & (len-1);
	//从下标i位置开始遍历找合适的位置 插入
     for (Entry e = tab[i];
          e != null;
          e = tab[i = nextIndex(i, len)]) {
         ThreadLocal<?> k = e.get();
		//如果map中已经存在要插入的key,那么就直接覆盖key对应的value
         if (k == key) {
             e.value = value;
             return;
         }
		//如果map中,下标i位置的元素key是空的,说明,key已经被GC回收掉了,那么就将当前要插入的key和value放到这里,将旧的
		//value值替换掉
         if (k == null) {
             //清理无用的旧值
             replaceStaleEntry(key, value, i);
             return;
         }
     }
	//如果map中下标i位置的元素直接是空的,那么就将当前要插入的key和value放到这里
     tab[i] = new Entry(key, value);
     int sz = ++size;
     if (!cleanSomeSlots(i, sz) && sz >= threshold)
     //如果map的大小达到了阈值
         rehash();
 }

方法总结:

  • ThreadLocal的set方法,就是往当前线程中存储key和value,key就是当前正在使用的ThreadLocal对象;value就是要存取的业务值,比如本demo中的accountId;
  • 如果要插入的key已经存在,那么使用新的value替换掉老的value;
  • 如果有Entry的key指向的对象已经是null(弱引用,对象被GC了),那么就插入key和value,同时清理一遍这种无用的旧值。
  • 如果上面两种情况不存在,比如第一次插入,那么就直接new一个Entry对象插入即可;
  • 最后要判断map的Entry数组容量,超过阈值就扩容,类似Hashmap那一套,不深入分析了。

3.2 get()

public T get() {
	//获取到当前线程
    Thread t = Thread.currentThread();
    //取出线程种的本地变量map
    ThreadLocalMap map = getMap(t);
    if (map != null) {
    	//知道当前threadLocal对象的key对用的Entry节点
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            //得到Entry节点中的value值
            T result = (T)e.value;
            return result;
        }
    }
    //如果还没初始化线程本地变量,也就是还没存过值,那么就进行初始化
    return setInitialValue();
}
//和上面set()方法初始化一样
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;
}

get()方法总结:

  • 从当前线程中得到本地变量map对象
  • map中可能有多个节点,比如本demo中,每个线程的threadLocalMap对象中有两个节点,即两对key和value,key分别是threadLocal1和threadLocal2;如果业务中使用的threadLocal1.get()方法,那么就是获取key为threadLocal1的map节点entry。
  • 找到entry,就返回其中的value,比如demo中的accountId;
  • 当然,如果get时发现线程中的threadLocalMap还是空的,也就是还没set进去过任何东西,那么就先初始化。

3.3 remove()

上文分析的时候,提到过使用threadLocal可能会产生内存泄漏,但是当在业务中使用完threadLocal,要使用remove()方法将其从内存删除。

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
    	//删除当前threadLocal对象的key和其对应的value,也就是从map中删除一对key和value
        m.remove(this);
}

关键方法是m.remove(this),这个是ThreadLocalMap类中的方法,继续跟进,

/**
 * Remove the entry for key.
 */
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) {
        	//这里清楚,就是让此节点等于null即可
            e.clear();
            //整理一遍map数组,清楚key已经是null的节点
            expungeStaleEntry(i);
            return;
        }
    }
}

内容比较简单,就是遍历map数组,根据key找到对应的entry,然后将其值设置为null即可。

本文就写到这里,综上,我们关键还是要结合demo理解ThreadLocal的使用场景,以及具体的运作流程。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值